Merge remote-tracking branch 'refs/remotes/upstream/1.20/dev' into feat/multi-loader-1.21

This commit is contained in:
IThundxr 2024-07-19 18:18:37 -04:00
commit 246939b6a5
No known key found for this signature in database
GPG Key ID: E291EC97BAF935E6
84 changed files with 1709 additions and 1690 deletions

View File

@ -9,6 +9,7 @@ import dev.engine_room.flywheel.api.instance.Instance;
import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.api.task.TaskExecutor;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.client.Camera;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
@ -61,6 +62,15 @@ public interface Engine {
*/
Vec3i renderOrigin();
/**
* Assign the set of sections that visuals have requested GPU light for.
*
* <p> This will be called at most once per frame, and not necessarily every frame.
*
* @param sections The set of sections.
*/
void lightSections(LongSet sections);
/**
* Free all resources associated with this engine.
* <br>

View File

@ -1,7 +1,5 @@
package dev.engine_room.flywheel.api.visual;
import java.util.List;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
/**
@ -16,5 +14,5 @@ public interface Effect {
* @param ctx The visualization context.
* @return An arbitrary EffectVisual.
*/
List<EffectVisual<?>> visualize(VisualizationContext ctx, float partialTick);
EffectVisual<?> visualize(VisualizationContext ctx, float partialTick);
}

View File

@ -0,0 +1,22 @@
package dev.engine_room.flywheel.api.visual;
/**
* A visual that listens to light updates.
*
* <p>If your visual moves around in the level at all, you should use {@link TickableVisual} or {@link DynamicVisual},
* and poll for light yourself along with listening for updates. When your visual moves to a different section, call
* {@link SectionCollector#sections}.</p>
*/
public non-sealed interface LightUpdatedVisual extends SectionTrackedVisual {
/**
* Called after visual construction and when a section this visual is contained in receives a light update.
*
* <p>Even if multiple sections are updated at the same time, this method will only be called once.</p>
*
* <p>The implementation is free to parallelize calls to this method, as well as execute the plan
* returned by {@link DynamicVisual#planFrame} simultaneously. It is safe to query/update light here,
* but you must ensure proper synchronization if you want to mutate anything outside this visual or
* anything that is also mutated within {@link DynamicVisual#planFrame}.</p>
*/
void updateLight(float partialTick);
}

View File

@ -1,62 +0,0 @@
package dev.engine_room.flywheel.api.visual;
import java.util.function.LongConsumer;
import net.minecraft.core.SectionPos;
/**
* A visual that listens to light updates.
*
* <p>If your visual moves around in the level at all, you should use {@link TickableVisual} or {@link DynamicVisual},
* and poll for light yourself along with listening for updates. When your visual moves to a different section, call
* {@link Notifier#notifySectionsChanged}.</p>
*/
public interface LitVisual extends Visual {
/**
* Set the notifier object.
*
* <p>This method is only called once right after the visual
* is created and before {@link #collectLightSections}.</p>
*
* @param notifier The notifier.
*/
void setLightSectionNotifier(Notifier notifier);
/**
* Collect the sections that this visual is contained in.
*
* <p>This method is called upon visual creation, and the frame after
* {@link Notifier#notifySectionsChanged} is called.</p>
*
* @param consumer The consumer to provide the sections to.
* @see SectionPos#asLong
*/
void collectLightSections(LongConsumer consumer);
/**
* Called when a section this visual is contained in receives a light update.
*
* <p>Even if multiple sections are updated at the same time, this method will only be called once.</p>
*
* <p>The implementation is free to parallelize calls to this method, as well as execute the plan
* returned by {@link DynamicVisual#planFrame} simultaneously. It is safe to query/update light here,
* but you must ensure proper synchronization if you want to mutate anything outside this visual or
* anything that is also mutated within {@link DynamicVisual#planFrame}.</p>
*
* <p>This method not is invoked automatically after visual creation.</p>
*/
void updateLight(float partialTick);
/**
* A notifier object that can be used to indicate to the impl
* that the sections a visual is contained in have changed.
*/
interface Notifier {
/**
* Invoke this to indicate to the impl that your visual has moved to a different set of sections.
* <br>
* The next frame, the impl will call {@link LitVisual#collectLightSections} again.
*/
void notifySectionsChanged();
}
}

View File

@ -0,0 +1,26 @@
package dev.engine_room.flywheel.api.visual;
import org.jetbrains.annotations.ApiStatus;
import it.unimi.dsi.fastutil.longs.LongSet;
public sealed interface SectionTrackedVisual extends Visual permits ShaderLightVisual, LightUpdatedVisual {
/**
* Set the section collector object.
*
* <p>This method is only called once, upon visual creation.
* <br>If the collector is invoked in this method, the
* visual will immediately be tracked in the given sections.
*
* @param collector The collector.
*/
void setSectionCollector(SectionCollector collector);
@ApiStatus.NonExtendable
interface SectionCollector {
/**
* Assign the set of sections this visual wants to track itself in.
*/
void sections(LongSet sections);
}
}

View File

@ -0,0 +1,14 @@
package dev.engine_room.flywheel.api.visual;
/**
* A marker interface allowing visuals to request light data on the GPU for a set of sections.
*
* <p> Sections passed into {@link SectionCollector#sections} will have their light data handed to the
* backend and queryable by {@code flw_light*} functions in shaders.
* <br>
* Note that the queryable light data is shared across all visuals, so even if one specific visual does not
* request a given section, the data will be available if another visual does.
*/
public non-sealed interface ShaderLightVisual extends SectionTrackedVisual {
}

View File

@ -5,7 +5,8 @@ package dev.engine_room.flywheel.api.visual;
*
* @see DynamicVisual
* @see TickableVisual
* @see LitVisual
* @see LightUpdatedVisual
* @see ShaderLightVisual
*/
public interface Visual {
/**

View File

@ -1,7 +1,5 @@
package dev.engine_room.flywheel.api.visualization;
import java.util.List;
import dev.engine_room.flywheel.api.visual.BlockEntityVisual;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.world.level.block.entity.BlockEntity;
@ -18,7 +16,7 @@ public interface BlockEntityVisualizer<T extends BlockEntity> {
* @param blockEntity The block entity to construct a visual for.
* @return The visual.
*/
List<BlockEntityVisual<? super T>> createVisual(VisualizationContext ctx, T blockEntity, float partialTick);
BlockEntityVisual<? super T> createVisual(VisualizationContext ctx, T blockEntity, float partialTick);
/**
* Checks if the given block entity should not be rendered with the vanilla {@link BlockEntityRenderer}.

View File

@ -1,7 +1,5 @@
package dev.engine_room.flywheel.api.visualization;
import java.util.List;
import dev.engine_room.flywheel.api.visual.EntityVisual;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.world.entity.Entity;
@ -18,7 +16,7 @@ public interface EntityVisualizer<T extends Entity> {
* @param entity The entity to construct a visual for.
* @return The visual.
*/
List<EntityVisual<? super T>> createVisual(VisualizationContext ctx, T entity, float partialTick);
EntityVisual<? super T> createVisual(VisualizationContext ctx, T entity, float partialTick);
/**
* Checks if the given entity should not render with the vanilla {@link EntityRenderer}.

View File

@ -4,7 +4,6 @@ import org.joml.Matrix3fc;
import org.joml.Matrix4fc;
import dev.engine_room.flywheel.api.BackendImplemented;
import net.minecraft.world.level.BlockAndTintGetter;
@BackendImplemented
public interface VisualEmbedding extends VisualizationContext {
@ -16,31 +15,6 @@ public interface VisualEmbedding extends VisualizationContext {
*/
void transforms(Matrix4fc pose, Matrix3fc normal);
/**
* Collect light information from the given level for the given box.
*
* <p>Call this method on as many or as few boxes as you need to
* encompass all child visuals of this embedding.</p>
*
* <p>After this method is called, instances rendered from this
* embedding within the given box will be lit as if they were in
* the given level.</p>
*
* @param level The level to collect light information from.
* @param minX The minimum x coordinate of the box.
* @param minY The minimum y coordinate of the box.
* @param minZ The minimum z coordinate of the box.
* @param sizeX The size of the box in the x direction.
* @param sizeY The size of the box in the y direction.
* @param sizeZ The size of the box in the z direction.
*/
void collectLight(BlockAndTintGetter level, int minX, int minY, int minZ, int sizeX, int sizeY, int sizeZ);
/**
* Reset any collected lighting information.
*/
void invalidateLight();
/**
* Delete this embedding.
*

View File

@ -16,7 +16,7 @@ public final class Backends {
* Use GPU instancing to render everything.
*/
public static final Backend INSTANCING = SimpleBackend.builder()
.engineFactory(level -> new EngineImpl(new InstancedDrawManager(InstancingPrograms.get()), 256))
.engineFactory(level -> new EngineImpl(level, new InstancedDrawManager(InstancingPrograms.get()), 256))
.supported(() -> GlCompat.SUPPORTS_INSTANCING && InstancingPrograms.allLoaded() && !ShadersModHandler.isShaderPackInUse())
.register(Flywheel.rl("instancing"));
@ -24,7 +24,7 @@ public final class Backends {
* Use Compute shaders to cull instances.
*/
public static final Backend INDIRECT = SimpleBackend.builder()
.engineFactory(level -> new EngineImpl(new IndirectDrawManager(IndirectPrograms.get()), 256))
.engineFactory(level -> new EngineImpl(level, new IndirectDrawManager(IndirectPrograms.get()), 256))
.fallback(() -> Backends.INSTANCING)
.supported(() -> GlCompat.SUPPORTS_INDIRECT && IndirectPrograms.allLoaded() && !ShadersModHandler.isShaderPackInUse())
.register(Flywheel.rl("indirect"));

View File

@ -0,0 +1,37 @@
package dev.engine_room.flywheel.backend;
import dev.engine_room.flywheel.lib.util.LevelAttached;
import it.unimi.dsi.fastutil.longs.LongArraySet;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.world.level.LevelAccessor;
/**
* Stores the set of updates light sections for LightStorage to poll in its frame plan.
*/
public class LightUpdateHolder {
private static final LevelAttached<LightUpdateHolder> HOLDERS = new LevelAttached<>(level -> new LightUpdateHolder());
private final LongSet updatedSections = new LongOpenHashSet();
private LightUpdateHolder() {
}
public static LightUpdateHolder get(LevelAccessor level) {
return HOLDERS.get(level);
}
public LongSet getAndClearUpdatedSections() {
if (updatedSections.isEmpty()) {
return LongSet.of();
}
var out = new LongArraySet(updatedSections);
updatedSections.clear();
return out;
}
public void add(long section) {
updatedSections.add(section);
}
}

View File

@ -8,5 +8,6 @@ public class Samplers {
public static final GlTextureUnit LIGHT = GlTextureUnit.T2;
public static final GlTextureUnit CRUMBLING = GlTextureUnit.T3;
public static final GlTextureUnit INSTANCE_BUFFER = GlTextureUnit.T4;
public static final GlTextureUnit EMBEDDED_LIGHT = GlTextureUnit.T5;
public static final GlTextureUnit LIGHT_LUT = GlTextureUnit.T5;
public static final GlTextureUnit LIGHT_SECTIONS = GlTextureUnit.T6;
}

View File

@ -13,7 +13,8 @@ public enum ContextShader {
DEFAULT(null, $ -> {
}),
CRUMBLING("_FLW_CRUMBLING", program -> program.setSamplerBinding("_flw_crumblingTex", Samplers.CRUMBLING)),
EMBEDDED("_FLW_EMBEDDED", program -> program.setSamplerBinding("_flw_lightVolume", Samplers.EMBEDDED_LIGHT));
EMBEDDED("FLW_EMBEDDED", $ -> {
});
@Nullable
private final String define;

View File

@ -11,7 +11,11 @@ public final class Pipelines {
.vertexMain(Flywheel.rl("internal/instancing/main.vert"))
.fragmentMain(Flywheel.rl("internal/instancing/main.frag"))
.assembler(BufferTextureInstanceComponent::new)
.onLink(program -> program.setSamplerBinding("_flw_instances", Samplers.INSTANCE_BUFFER))
.onLink(program -> {
program.setSamplerBinding("_flw_instances", Samplers.INSTANCE_BUFFER);
program.setSamplerBinding("_flw_lightLut", Samplers.LIGHT_LUT);
program.setSamplerBinding("_flw_lightSections", Samplers.LIGHT_SECTIONS);
})
.build();
public static final Pipeline INDIRECT = Pipeline.builder()

View File

@ -5,7 +5,7 @@ import java.util.ArrayList;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.api.instance.InstanceType;
import dev.engine_room.flywheel.api.layout.Layout;
import dev.engine_room.flywheel.backend.engine.indirect.IndirectBuffers;
import dev.engine_room.flywheel.backend.engine.indirect.BufferBindings;
import dev.engine_room.flywheel.backend.glsl.generate.FnSignature;
import dev.engine_room.flywheel.backend.glsl.generate.GlslBlock;
import dev.engine_room.flywheel.backend.glsl.generate.GlslBuilder;
@ -43,7 +43,7 @@ public class SsboInstanceComponent extends InstanceAssemblerComponent {
fnBody.ret(GlslExpr.call(STRUCT_NAME, unpackArgs));
builder._addRaw("layout(std430, binding = " + IndirectBuffers.INSTANCE_INDEX + ") restrict readonly buffer InstanceBuffer {\n"
builder._addRaw("layout(std430, binding = " + BufferBindings.INSTANCE + ") restrict readonly buffer InstanceBuffer {\n"
+ " uint _flw_instances[];\n"
+ "};");
builder.blankLine();

View File

@ -25,8 +25,6 @@ public abstract class AbstractInstancer<I extends Instance> implements Instancer
protected AbstractInstancer(InstanceType<I> type, Environment environment) {
this.type = type;
this.environment = environment;
environment.acquire();
}
@Override
@ -177,9 +175,7 @@ public abstract class AbstractInstancer<I extends Instance> implements Instancer
deleted.clear();
}
public void delete() {
environment.release();
}
public abstract void delete();
@Override
public String toString() {

View File

@ -0,0 +1,54 @@
package dev.engine_room.flywheel.backend.engine;
import dev.engine_room.flywheel.lib.memory.MemoryBlock;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
public class Arena {
private final long elementSizeBytes;
private MemoryBlock memoryBlock;
// Monotonic index, generally represents the size of the arena.
private int top = 0;
// List of free indices.
private final IntList freeStack = new IntArrayList();
public Arena(long elementSizeBytes, int initialCapacity) {
this.elementSizeBytes = elementSizeBytes;
memoryBlock = MemoryBlock.malloc(elementSizeBytes * initialCapacity);
}
public int alloc() {
// First re-use freed elements.
if (!freeStack.isEmpty()) {
return freeStack.removeInt(freeStack.size() - 1);
}
// Make sure there's room to increment top.
if (top * elementSizeBytes >= memoryBlock.size()) {
memoryBlock = memoryBlock.realloc(memoryBlock.size() * 2);
}
// Return the top index and increment.
return top++;
}
public void free(int i) {
// That's it! Now pls don't try to use it.
freeStack.add(i);
}
public long indexToPointer(int i) {
return memoryBlock.ptr() + i * elementSizeBytes;
}
public void delete() {
memoryBlock.free();
}
public int capacity() {
return top;
}
}

View File

@ -45,7 +45,7 @@ public abstract class DrawManager<N extends AbstractInstancer<?>> {
initializationQueue.clear();
}
public void flush() {
public void flush(LightStorage lightStorage) {
// Thread safety: flush is called from the render thread after all visual updates have been made,
// so there are no:tm: threads we could be racing with.
for (var instancer : initializationQueue) {

View File

@ -14,34 +14,46 @@ import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.api.task.TaskExecutor;
import dev.engine_room.flywheel.api.visualization.VisualEmbedding;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
import dev.engine_room.flywheel.backend.engine.embed.EmbeddedEnvironment;
import dev.engine_room.flywheel.backend.engine.embed.Environment;
import dev.engine_room.flywheel.backend.engine.embed.TopLevelEmbeddedEnvironment;
import dev.engine_room.flywheel.backend.engine.embed.EnvironmentStorage;
import dev.engine_room.flywheel.backend.engine.uniform.Uniforms;
import dev.engine_room.flywheel.backend.gl.GlStateTracker;
import dev.engine_room.flywheel.lib.task.Flag;
import dev.engine_room.flywheel.lib.task.NamedFlag;
import dev.engine_room.flywheel.lib.task.SyncedPlan;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.client.Camera;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.phys.Vec3;
public class EngineImpl implements Engine {
private final int sqrMaxOriginDistance;
private final DrawManager<? extends AbstractInstancer<?>> drawManager;
private final EnvironmentStorage environmentStorage = new EnvironmentStorage();
private final int sqrMaxOriginDistance;
private final Flag flushFlag = new NamedFlag("flushed");
private final EnvironmentStorage environmentStorage;
private final LightStorage lightStorage;
private BlockPos renderOrigin = BlockPos.ZERO;
public EngineImpl(DrawManager<? extends AbstractInstancer<?>> drawManager, int maxOriginDistance) {
public EngineImpl(LevelAccessor level, DrawManager<? extends AbstractInstancer<?>> drawManager, int maxOriginDistance) {
this.drawManager = drawManager;
sqrMaxOriginDistance = maxOriginDistance * maxOriginDistance;
environmentStorage = new EnvironmentStorage();
lightStorage = new LightStorage(level);
}
@Override
public VisualizationContext createVisualizationContext(RenderStage stage) {
return new VisualizationContextImpl(stage);
}
@Override
public Plan<RenderContext> createFramePlan() {
return SyncedPlan.of(this::flush);
return lightStorage.createFramePlan()
.then(SyncedPlan.of(this::flush));
}
@Override
@ -62,11 +74,6 @@ public class EngineImpl implements Engine {
drawManager.renderCrumbling(crumblingBlocks);
}
@Override
public VisualizationContext createVisualizationContext(RenderStage stage) {
return new VisualizationContextImpl(stage);
}
@Override
public boolean updateRenderOrigin(Camera camera) {
Vec3 cameraPos = camera.getPosition();
@ -89,10 +96,15 @@ public class EngineImpl implements Engine {
return renderOrigin;
}
@Override
public void lightSections(LongSet sections) {
lightStorage.sections(sections);
}
@Override
public void delete() {
drawManager.delete();
environmentStorage.delete();
lightStorage.delete();
}
public <I extends Instance> Instancer<I> instancer(Environment environment, InstanceType<I> type, Model model, RenderStage stage) {
@ -102,8 +114,8 @@ public class EngineImpl implements Engine {
private void flush(RenderContext ctx) {
try (var state = GlStateTracker.getRestoreState()) {
Uniforms.update(ctx);
drawManager.flush();
environmentStorage.flush();
drawManager.flush(lightStorage);
}
flushFlag.raise();
@ -113,6 +125,10 @@ public class EngineImpl implements Engine {
return environmentStorage;
}
public LightStorage lightStorage() {
return lightStorage;
}
private class VisualizationContextImpl implements VisualizationContext {
private final InstancerProviderImpl instancerProvider;
private final RenderStage stage;
@ -134,7 +150,7 @@ public class EngineImpl implements Engine {
@Override
public VisualEmbedding createEmbedding() {
var out = new TopLevelEmbeddedEnvironment(EngineImpl.this, stage);
var out = new EmbeddedEnvironment(EngineImpl.this, stage);
environmentStorage.track(out);
return out;
}

View File

@ -1,39 +0,0 @@
package dev.engine_room.flywheel.backend.engine;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import dev.engine_room.flywheel.backend.engine.embed.AbstractEmbeddedEnvironment;
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
import it.unimi.dsi.fastutil.objects.ReferenceSet;
import it.unimi.dsi.fastutil.objects.ReferenceSets;
public class EnvironmentStorage {
protected final ReferenceSet<AbstractEmbeddedEnvironment> environments = ReferenceSets.synchronize(new ReferenceLinkedOpenHashSet<>());
private final Queue<AbstractEmbeddedEnvironment> forDeletion = new ConcurrentLinkedQueue<>();
public void track(AbstractEmbeddedEnvironment environment) {
environments.add(environment);
}
public void enqueueDeletion(AbstractEmbeddedEnvironment environment) {
environments.remove(environment);
forDeletion.add(environment);
}
public void flush() {
AbstractEmbeddedEnvironment env;
while ((env = forDeletion.poll()) != null) {
env.actuallyDelete();
}
environments.forEach(AbstractEmbeddedEnvironment::flush);
}
public void delete() {
environments.forEach(AbstractEmbeddedEnvironment::actuallyDelete);
environments.clear();
}
}

View File

@ -0,0 +1,142 @@
package dev.engine_room.flywheel.backend.engine;
import org.jetbrains.annotations.NotNull;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntObjectImmutablePair;
import it.unimi.dsi.fastutil.ints.IntObjectPair;
import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongComparator;
import it.unimi.dsi.fastutil.objects.ReferenceArrayList;
import net.minecraft.core.SectionPos;
public final class LightLut {
private static final LongComparator SECTION_X_THEN_Y_THEN_Z = (long a, long b) -> {
final var xComp = Integer.compare(SectionPos.x(a), SectionPos.x(b));
if (xComp != 0) {
return xComp;
}
var yComp = Integer.compare(SectionPos.y(a), SectionPos.y(b));
if (yComp != 0) {
return yComp;
}
return Integer.compare(SectionPos.z(a), SectionPos.z(b));
};
private LightLut() {
}
// Massive kudos to RogueLogix for figuring out this LUT scheme.
// TODO: switch to y x z or x z y ordering
// DATA LAYOUT
// [0] : base chunk X, X index count, followed by linear indices of y blocks
// [yBlockIndex] : baseChunk Y, Y index count, followed by linear indices of z blocks for this x
// [zBlockIndex] : baseChunk Z, Z index count, followed by linear indices of lighting chunks
// this data layout allows a single buffer to represent the lighting volume, without requiring the entire 3d lookup volume to be allocated
public static IntArrayList buildLut(Long2IntMap sectionIndicesMaps) {
if (sectionIndicesMaps.isEmpty()) {
return new IntArrayList();
}
final var positions = sortedKeys(sectionIndicesMaps);
final var baseX = SectionPos.x(positions.getLong(0));
return buildLut(baseX, buildIndices(sectionIndicesMaps, positions, baseX));
}
private static ReferenceArrayList<IntObjectPair<ReferenceArrayList<IntArrayList>>> buildIndices(Long2IntMap sectionIndicesMaps, LongArrayList positions, int baseX) {
final var indices = new ReferenceArrayList<IntObjectPair<ReferenceArrayList<IntArrayList>>>();
for (long position : positions) {
final var x = SectionPos.x(position);
final var y = SectionPos.y(position);
final var z = SectionPos.z(position);
final var xIndex = x - baseX;
if (indices.size() <= xIndex) {
indices.ensureCapacity(xIndex + 1);
indices.size(xIndex + 1);
}
var yLookup = indices.get(xIndex);
if (yLookup == null) {
//noinspection SuspiciousNameCombination
yLookup = new IntObjectImmutablePair<>(y, new ReferenceArrayList<>());
indices.set(xIndex, yLookup);
}
final var yIndices = yLookup.right();
final var yIndex = y - yLookup.leftInt();
if (yIndices.size() <= yIndex) {
yIndices.ensureCapacity(yIndex + 1);
yIndices.size(yIndex + 1);
}
var zLookup = yIndices.get(yIndex);
if (zLookup == null) {
zLookup = new IntArrayList();
zLookup.add(z);
zLookup.add(0); // this value will be filled in later
yIndices.set(yIndex, zLookup);
}
final var zIndex = z - zLookup.getInt(0);
if ((zLookup.size() - 2) <= zIndex) {
zLookup.ensureCapacity(zIndex + 3);
zLookup.size(zIndex + 3);
}
// Add 1 to the actual index so that 0 indicates a missing section.
zLookup.set(zIndex + 2, sectionIndicesMaps.get(position) + 1);
}
return indices;
}
private static @NotNull LongArrayList sortedKeys(Long2IntMap sectionIndicesMaps) {
final var out = new LongArrayList(sectionIndicesMaps.keySet());
out.unstableSort(SECTION_X_THEN_Y_THEN_Z);
return out;
}
private static IntArrayList buildLut(int baseX, ReferenceArrayList<IntObjectPair<ReferenceArrayList<IntArrayList>>> indices) {
final var out = new IntArrayList();
out.add(baseX);
out.add(indices.size());
for (int i = 0; i < indices.size(); i++) {
out.add(0);
}
for (int x = 0; x < indices.size(); x++) {
final var yLookup = indices.get(x);
if (yLookup == null) {
out.set(x + 2, 0);
continue;
}
// ensure that the base position and size dont cross a (64 byte) cache line
if ((out.size() & 0xF) == 0xF) {
out.add(0);
}
final var baseYIndex = out.size();
out.set(x + 2, baseYIndex);
final var yIndices = yLookup.right();
out.add(yLookup.leftInt());
out.add(yIndices.size());
for (int i = 0; i < indices.size(); i++) {
out.add(0);
}
for (int y = 0; y < yIndices.size(); y++) {
final var zLookup = yIndices.get(y);
if (zLookup == null) {
out.set(baseYIndex + y + 2, 0);
continue;
}
// ensure that the base position and size dont cross a (64 byte) cache line
if ((out.size() & 0xF) == 0xF) {
out.add(0);
}
out.set(baseYIndex + y + 2, out.size());
zLookup.set(1, zLookup.size() - 2);
out.addAll(zLookup);
}
}
return out;
}
}

View File

@ -0,0 +1,385 @@
package dev.engine_room.flywheel.backend.engine;
import java.util.BitSet;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import dev.engine_room.flywheel.api.event.RenderContext;
import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.backend.LightUpdateHolder;
import dev.engine_room.flywheel.backend.engine.indirect.StagingBuffer;
import dev.engine_room.flywheel.backend.gl.buffer.GlBuffer;
import dev.engine_room.flywheel.lib.task.SimplePlan;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArraySet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.lighting.LayerLightEventListener;
/**
* TODO: AO data
* A managed arena of light sections for uploading to the GPU.
*
* <p>Each section represents an 18x18x18 block volume of light data.
* The "edges" are taken from the neighboring sections, so that each
* shader invocation only needs to access a single section of data.
* Even still, neighboring shader invocations may need to access other sections.
*
* <p>Sections are logically stored as a 9x9x9 array of longs,
* where each long holds a 2x2x2 array of light data.
* <br>Both the greater array and the longs are packed in x, z, y order.
*
* <p>Thus, each section occupies 5832 bytes.
*/
public class LightStorage {
public static final long SECTION_SIZE_BYTES = 9 * 9 * 9 * 8;
private static final int DEFAULT_ARENA_CAPACITY_SECTIONS = 64;
private static final int INVALID_SECTION = -1;
private final LevelAccessor level;
private final Arena arena;
private final Long2IntMap section2ArenaIndex = new Long2IntOpenHashMap();
{
section2ArenaIndex.defaultReturnValue(INVALID_SECTION);
}
private final BitSet changed = new BitSet();
private boolean needsLutRebuild = false;
@Nullable
private LongSet requestedSections;
public LightStorage(LevelAccessor level) {
this.level = level;
arena = new Arena(SECTION_SIZE_BYTES, DEFAULT_ARENA_CAPACITY_SECTIONS);
}
/**
* Set the set of requested sections.
* <p> When set, this will be processed in the next frame plan. It may not be set every frame.
*
* @param sections The set of sections requested by the impl.
*/
public void sections(LongSet sections) {
requestedSections = sections;
}
public Plan<RenderContext> createFramePlan() {
return SimplePlan.of(() -> {
var updatedSections = LightUpdateHolder.get(level)
.getAndClearUpdatedSections();
if (updatedSections.isEmpty() && requestedSections == null) {
return;
}
removeUnusedSections();
// Start building the set of sections we need to collect this frame.
LongSet sectionsToCollect;
if (requestedSections == null) {
// If none were requested, then we need to collect all sections that received updates.
sectionsToCollect = new LongArraySet();
} else {
// If we did receive a new set of requested sections, we only
// need to collect the sections that weren't yet tracked.
sectionsToCollect = requestedSections;
sectionsToCollect.removeAll(section2ArenaIndex.keySet());
}
// updatedSections contains all sections than received light updates,
// but we only care about its intersection with our tracked sections.
for (long updatedSection : updatedSections) {
// Since sections contain the border light of their neighbors, we need to collect the neighbors as well.
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
for (int z = -1; z <= 1; z++) {
long section = SectionPos.offset(updatedSection, x, y, z);
if (section2ArenaIndex.containsKey(section)) {
sectionsToCollect.add(section);
}
}
}
}
}
// Now actually do the collection.
// TODO: Should this be done in parallel?
sectionsToCollect.forEach(this::collectSection);
requestedSections = null;
});
}
private void removeUnusedSections() {
if (requestedSections == null) {
return;
}
var entries = section2ArenaIndex.long2IntEntrySet();
var it = entries.iterator();
while (it.hasNext()) {
var entry = it.next();
var section = entry.getLongKey();
if (!this.requestedSections.contains(section)) {
arena.free(entry.getIntValue());
needsLutRebuild = true;
it.remove();
}
}
}
public int capacity() {
return arena.capacity();
}
public void collectSection(long section) {
var lightEngine = level.getLightEngine();
var blockLight = lightEngine.getLayerListener(LightLayer.BLOCK);
var skyLight = lightEngine.getLayerListener(LightLayer.SKY);
int index = indexForSection(section);
changed.set(index);
long ptr = arena.indexToPointer(index);
// Zero it out first. This is basically free and makes it easier to handle missing sections later.
MemoryUtil.memSet(ptr, 0, SECTION_SIZE_BYTES);
collectCenter(blockLight, skyLight, ptr, section);
for (SectionEdge i : SectionEdge.values()) {
collectYZPlane(blockLight, skyLight, ptr, SectionPos.offset(section, i.sectionOffset, 0, 0), i);
collectXZPlane(blockLight, skyLight, ptr, SectionPos.offset(section, 0, i.sectionOffset, 0), i);
collectXYPlane(blockLight, skyLight, ptr, SectionPos.offset(section, 0, 0, i.sectionOffset), i);
for (SectionEdge j : SectionEdge.values()) {
collectXStrip(blockLight, skyLight, ptr, SectionPos.offset(section, 0, i.sectionOffset, j.sectionOffset), i, j);
collectYStrip(blockLight, skyLight, ptr, SectionPos.offset(section, i.sectionOffset, 0, j.sectionOffset), i, j);
collectZStrip(blockLight, skyLight, ptr, SectionPos.offset(section, i.sectionOffset, j.sectionOffset, 0), i, j);
}
}
collectCorners(blockLight, skyLight, ptr, section);
}
private void collectXStrip(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section, SectionEdge y, SectionEdge z) {
var pos = SectionPos.of(section);
var blockData = blockLight.getDataLayerData(pos);
var skyData = skyLight.getDataLayerData(pos);
if (blockData == null || skyData == null) {
return;
}
for (int x = 0; x < 16; x++) {
write(ptr, x, y.relative, z.relative, blockData.get(x, y.pos, z.pos), skyData.get(x, y.pos, z.pos));
}
}
private void collectYStrip(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section, SectionEdge x, SectionEdge z) {
var pos = SectionPos.of(section);
var blockData = blockLight.getDataLayerData(pos);
var skyData = skyLight.getDataLayerData(pos);
if (blockData == null || skyData == null) {
return;
}
for (int y = 0; y < 16; y++) {
write(ptr, x.relative, y, z.relative, blockData.get(x.pos, y, z.pos), skyData.get(x.pos, y, z.pos));
}
}
private void collectZStrip(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section, SectionEdge x, SectionEdge y) {
var pos = SectionPos.of(section);
var blockData = blockLight.getDataLayerData(pos);
var skyData = skyLight.getDataLayerData(pos);
if (blockData == null || skyData == null) {
return;
}
for (int z = 0; z < 16; z++) {
write(ptr, x.relative, y.relative, z, blockData.get(x.pos, y.pos, z), skyData.get(x.pos, y.pos, z));
}
}
private void collectYZPlane(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section, SectionEdge x) {
var pos = SectionPos.of(section);
var blockData = blockLight.getDataLayerData(pos);
var skyData = skyLight.getDataLayerData(pos);
if (blockData == null || skyData == null) {
return;
}
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
write(ptr, x.relative, y, z, blockData.get(x.pos, y, z), skyData.get(x.pos, y, z));
}
}
}
private void collectXZPlane(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section, SectionEdge y) {
var pos = SectionPos.of(section);
var blockData = blockLight.getDataLayerData(pos);
var skyData = skyLight.getDataLayerData(pos);
if (blockData == null || skyData == null) {
return;
}
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
write(ptr, x, y.relative, z, blockData.get(x, y.pos, z), skyData.get(x, y.pos, z));
}
}
}
private void collectXYPlane(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section, SectionEdge z) {
var pos = SectionPos.of(section);
var blockData = blockLight.getDataLayerData(pos);
var skyData = skyLight.getDataLayerData(pos);
if (blockData == null || skyData == null) {
return;
}
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
write(ptr, x, y, z.relative, blockData.get(x, y, z.pos), skyData.get(x, y, z.pos));
}
}
}
private void collectCenter(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section) {
var pos = SectionPos.of(section);
var blockData = blockLight.getDataLayerData(pos);
var skyData = skyLight.getDataLayerData(pos);
if (blockData == null || skyData == null) {
return;
}
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
write(ptr, x, y, z, blockData.get(x, y, z), skyData.get(x, y, z));
}
}
}
}
private void collectCorners(LayerLightEventListener blockLight, LayerLightEventListener skyLight, long ptr, long section) {
var blockPos = new BlockPos.MutableBlockPos();
int xMin = SectionPos.sectionToBlockCoord(SectionPos.x(section));
int yMin = SectionPos.sectionToBlockCoord(SectionPos.y(section));
int zMin = SectionPos.sectionToBlockCoord(SectionPos.z(section));
for (SectionEdge x : SectionEdge.values()) {
for (SectionEdge y : SectionEdge.values()) {
for (SectionEdge z : SectionEdge.values()) {
blockPos.set(x.relative + xMin, y.relative + yMin, z.relative + zMin);
write(ptr, x.relative, y.relative, z.relative, blockLight.getLightValue(blockPos), skyLight.getLightValue(blockPos));
}
}
}
}
/**
* Write to the given section.
* @param ptr Pointer to the base of a section's data.
* @param x X coordinate in the section, from [-1, 16].
* @param y Y coordinate in the section, from [-1, 16].
* @param z Z coordinate in the section, from [-1, 16].
* @param block The block light level, from [0, 15].
* @param sky The sky light level, from [0, 15].
*/
private void write(long ptr, int x, int y, int z, int block, int sky) {
int x1 = x + 1;
int y1 = y + 1;
int z1 = z + 1;
int offset = x1 + z1 * 18 + y1 * 18 * 18;
long packedByte = (block & 0xF) | ((sky & 0xF) << 4);
MemoryUtil.memPutByte(ptr + offset, (byte) packedByte);
}
/**
* Get a pointer to the base of the given section.
* <p> If the section is not yet reserved, allocate a chunk in the arena.
* @param section The section to write to.
* @return A raw pointer to the base of the section.
*/
private long ptrForSection(long section) {
return arena.indexToPointer(indexForSection(section));
}
private int indexForSection(long section) {
int out = section2ArenaIndex.get(section);
// Need to allocate.
if (out == INVALID_SECTION) {
out = arena.alloc();
section2ArenaIndex.put(section, out);
needsLutRebuild = true;
}
return out;
}
public void delete() {
arena.delete();
}
public boolean checkNeedsLutRebuildAndClear() {
var out = needsLutRebuild;
needsLutRebuild = false;
return out;
}
public void uploadChangedSections(StagingBuffer staging, int dstVbo) {
for (int i = changed.nextSetBit(0); i >= 0; i = changed.nextSetBit(i + 1)) {
staging.enqueueCopy(arena.indexToPointer(i), SECTION_SIZE_BYTES, dstVbo, i * SECTION_SIZE_BYTES);
}
changed.clear();
}
public void upload(GlBuffer buffer) {
if (changed.isEmpty()) {
return;
}
buffer.upload(arena.indexToPointer(0), arena.capacity() * SECTION_SIZE_BYTES);
changed.clear();
}
public IntArrayList createLut() {
// TODO: incremental lut updates
return LightLut.buildLut(section2ArenaIndex);
}
private enum SectionEdge {
LOW(15, -1, -1),
HIGH(0, 16, 1),
;
/**
* The position in the section to collect.
*/
private final int pos;
/**
* The position relative to the main section.
*/
private final int relative;
/**
* The offset to the neighboring section.
*/
private final int sectionOffset;
SectionEdge(int pos, int relative, int sectionOffset) {
this.pos = pos;
this.relative = relative;
this.sectionOffset = sectionOffset;
}
}
}

View File

@ -1,5 +1,6 @@
package dev.engine_room.flywheel.backend.engine.embed;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3f;
import org.joml.Matrix3fc;
import org.joml.Matrix4f;
@ -15,32 +16,38 @@ import dev.engine_room.flywheel.api.visualization.VisualEmbedding;
import dev.engine_room.flywheel.backend.compile.ContextShader;
import dev.engine_room.flywheel.backend.engine.EngineImpl;
import dev.engine_room.flywheel.backend.gl.shader.GlProgram;
import dev.engine_room.flywheel.backend.util.AtomicReferenceCounted;
import net.minecraft.core.Vec3i;
public abstract class AbstractEmbeddedEnvironment extends AtomicReferenceCounted implements Environment, VisualEmbedding {
protected final Matrix4f pose = new Matrix4f();
protected final Matrix3f normal = new Matrix3f();
private final Matrix4f poseComposed = new Matrix4f();
private final Matrix3f normalComposed = new Matrix3f();
private final InstancerProvider instancerProvider;
public class EmbeddedEnvironment implements VisualEmbedding, Environment {
private final EngineImpl engine;
private final RenderStage renderStage;
@Nullable
private final EmbeddedEnvironment parent;
private final InstancerProvider instancerProvider;
public AbstractEmbeddedEnvironment(EngineImpl engine, RenderStage renderStage) {
private final Matrix4f pose = new Matrix4f();
private final Matrix3f normal = new Matrix3f();
private final Matrix4f poseComposed = new Matrix4f();
private final Matrix3f normalComposed = new Matrix3f();
private boolean deleted = false;
public EmbeddedEnvironment(EngineImpl engine, RenderStage renderStage, @Nullable EmbeddedEnvironment parent) {
this.engine = engine;
this.renderStage = renderStage;
this.parent = parent;
instancerProvider = new InstancerProvider() {
@Override
public <I extends Instance> Instancer<I> instancer(InstanceType<I> type, Model model) {
// Kinda cursed usage of anonymous classes here, but it does the job.
return engine.instancer(AbstractEmbeddedEnvironment.this, type, model, renderStage);
return engine.instancer(EmbeddedEnvironment.this, type, model, renderStage);
}
};
}
// Acquire the reference owned by the visual that created this.
acquire();
public EmbeddedEnvironment(EngineImpl engine, RenderStage renderStage) {
this(engine, renderStage, null);
}
@Override
@ -49,33 +56,6 @@ public abstract class AbstractEmbeddedEnvironment extends AtomicReferenceCounted
this.normal.set(normal);
}
public void flush() {
poseComposed.identity();
normalComposed.identity();
composeMatrices(poseComposed, normalComposed);
}
@Override
public void setupDraw(GlProgram program) {
setupLight(program);
program.setMat4(EmbeddingUniforms.MODEL_MATRIX, poseComposed);
program.setMat3(EmbeddingUniforms.NORMAL_MATRIX, normalComposed);
}
@Override
public void setupCull(GlProgram program) {
program.setBool(EmbeddingUniforms.USE_MODEL_MATRIX, true);
program.setMat4(EmbeddingUniforms.MODEL_MATRIX1, poseComposed);
}
@Override
public ContextShader contextShader() {
return ContextShader.EMBEDDED;
}
@Override
public InstancerProvider instancerProvider() {
return instancerProvider;
@ -88,37 +68,56 @@ public abstract class AbstractEmbeddedEnvironment extends AtomicReferenceCounted
@Override
public VisualEmbedding createEmbedding() {
var out = new NestedEmbeddedEnvironment(this, engine, renderStage);
var out = new EmbeddedEnvironment(engine, renderStage, this);
engine.environmentStorage()
.track(out);
return out;
}
@Override
public ContextShader contextShader() {
return ContextShader.EMBEDDED;
}
@Override
public void setupCull(GlProgram program) {
program.setBool(EmbeddingUniforms.USE_MODEL_MATRIX, true);
program.setMat4(EmbeddingUniforms.MODEL_MATRIX, poseComposed);
}
@Override
public void setupDraw(GlProgram program) {
program.setMat4(EmbeddingUniforms.MODEL_MATRIX, poseComposed);
program.setMat3(EmbeddingUniforms.NORMAL_MATRIX, normalComposed);
}
public void flush() {
poseComposed.identity();
normalComposed.identity();
composeMatrices(poseComposed, normalComposed);
}
private void composeMatrices(Matrix4f pose, Matrix3f normal) {
if (parent != null) {
parent.composeMatrices(pose, normal);
pose.mul(this.pose);
normal.mul(this.normal);
} else {
pose.set(this.pose);
normal.set(this.normal);
}
}
public boolean isDeleted() {
return deleted;
}
/**
* Called by visuals
*/
@Override
public void delete() {
// Release the reference owned by the visual that created this.
// Note that visuals don't explicitly call acquire, instead the
// storage acquired a reference when this was constructed.
release();
deleted = true;
}
/**
* Called when referenceCount goes to 0
*/
@Override
public void _delete() {
engine.environmentStorage().enqueueDeletion(this);
}
public abstract void setupLight(GlProgram program);
public abstract void composeMatrices(Matrix4f pose, Matrix3f normal);
/**
* Called in EnvironmentStorage#flush
*/
public abstract void actuallyDelete();
}

View File

@ -1,87 +0,0 @@
package dev.engine_room.flywheel.backend.engine.embed;
import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T;
import static org.lwjgl.opengl.GL11.GL_UNPACK_ALIGNMENT;
import static org.lwjgl.opengl.GL11.GL_UNPACK_ROW_LENGTH;
import static org.lwjgl.opengl.GL11.GL_UNPACK_SKIP_PIXELS;
import static org.lwjgl.opengl.GL11.GL_UNPACK_SKIP_ROWS;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glPixelStorei;
import static org.lwjgl.opengl.GL11.glTexParameteri;
import static org.lwjgl.opengl.GL12.GL_TEXTURE_3D;
import static org.lwjgl.opengl.GL12.GL_TEXTURE_WRAP_R;
import static org.lwjgl.opengl.GL12.GL_UNPACK_IMAGE_HEIGHT;
import static org.lwjgl.opengl.GL12.GL_UNPACK_SKIP_IMAGES;
import static org.lwjgl.opengl.GL12.glTexImage3D;
import static org.lwjgl.opengl.GL12.glTexSubImage3D;
import static org.lwjgl.opengl.GL14.GL_MIRRORED_REPEAT;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.opengl.GL30;
import dev.engine_room.flywheel.backend.gl.GlTexture;
import net.minecraft.util.Mth;
public class EmbeddedLightTexture {
@Nullable
private GlTexture texture;
public int sizeX;
public int sizeY;
public int sizeZ;
public void bind() {
texture().bind();
}
private GlTexture texture() {
if (texture == null) {
texture = new GlTexture(GL_TEXTURE_3D);
}
return texture;
}
public void ensureCapacity(int sizeX, int sizeY, int sizeZ) {
sizeX = Mth.smallestEncompassingPowerOfTwo(sizeX);
sizeY = Mth.smallestEncompassingPowerOfTwo(sizeY);
sizeZ = Mth.smallestEncompassingPowerOfTwo(sizeZ);
if (sizeX > this.sizeX || sizeY > this.sizeY || sizeZ > this.sizeZ) {
this.sizeX = sizeX;
this.sizeY = sizeY;
this.sizeZ = sizeZ;
glTexImage3D(GL_TEXTURE_3D, 0, GL30.GL_RG8, sizeX, sizeY, sizeZ, 0, GL30.GL_RG, GL_UNSIGNED_BYTE, 0);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
}
}
public void upload(long ptr, int sizeX, int sizeY, int sizeZ) {
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, (int) EmbeddedLightVolume.STRIDE);
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, sizeX, sizeY, sizeZ, GL30.GL_RG, GL_UNSIGNED_BYTE, ptr);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // 4 is the default
}
public void delete() {
if (texture != null) {
texture.delete();
}
}
}

View File

@ -1,187 +0,0 @@
package dev.engine_room.flywheel.backend.engine.embed;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import dev.engine_room.flywheel.lib.memory.MemoryBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.LightLayer;
public class EmbeddedLightVolume {
public static final long STRIDE = Short.BYTES;
private int minX;
private int minY;
private int minZ;
private int maxX;
private int maxY;
private int maxZ;
private final BlockPos.MutableBlockPos scratchPos = new BlockPos.MutableBlockPos();
@Nullable
protected MemoryBlock memoryBlock;
protected boolean empty = true;
public boolean empty() {
return empty;
}
public void collect(BlockAndTintGetter level, int minX, int minY, int minZ, int sizeX, int sizeY, int sizeZ) {
maybeExpandForBox(minX, minY, minZ, sizeX, sizeY, sizeZ);
empty = false;
for (int z = minZ; z < minZ + sizeZ; z++) {
for (int y = minY; y < minY + sizeY; y++) {
for (int x = minX; x < minX + sizeX; x++) {
paintLight(level, x, y, z);
}
}
}
}
private void paintLight(BlockAndTintGetter level, int x, int y, int z) {
scratchPos.set(x, y, z);
int block = level.getBrightness(LightLayer.BLOCK, scratchPos);
int sky = level.getBrightness(LightLayer.SKY, scratchPos);
long ptr = this.memoryBlock.ptr() + offset(x - x(), y - y(), z - z(), sizeX(), sizeY());
MemoryUtil.memPutShort(ptr, (short) ((block << 4) | sky << 12));
}
private void maybeExpandForBox(int x, int y, int z, int sizeX, int sizeY, int sizeZ) {
if (empty || memoryBlock == null) {
// We're either brand new or recently #clear'd,
// so none of the previous min/max values have any meaning.
this.minX = x;
this.minY = y;
this.minZ = z;
this.maxX = x + sizeX;
this.maxY = y + sizeY;
this.maxZ = z + sizeZ;
int volume = sizeX * sizeY * sizeZ;
long neededSize = volume * STRIDE;
if (memoryBlock == null) {
memoryBlock = MemoryBlock.malloc(neededSize);
} else if (memoryBlock.size() < neededSize) {
// There's some memory left over from before the last #clear,
// but not enough to hold this initial box. Need to grow the block.
memoryBlock.realloc(neededSize);
}
// else: we have enough memory left over to hold this box, nothing to do!
return;
}
int oldMinX = this.minX;
int oldMinY = this.minY;
int oldMinZ = this.minZ;
int oldSizeX = this.sizeX();
int oldSizeY = this.sizeY();
int oldSizeZ = this.sizeZ();
boolean changed = false;
if (x < this.minX) {
this.minX = x;
changed = true;
}
if (y < this.minY) {
this.minY = y;
changed = true;
}
if (z < this.minZ) {
this.minZ = z;
changed = true;
}
if (x + sizeX > this.maxX) {
this.maxX = x + sizeX;
changed = true;
}
if (y + sizeY > this.maxY) {
this.maxY = y + sizeY;
changed = true;
}
if (z + sizeZ > this.maxZ) {
this.maxZ = z + sizeZ;
changed = true;
}
if (!changed) {
return;
}
int volume = volume();
memoryBlock = memoryBlock.realloc(volume * STRIDE);
int xOff = oldMinX - minX;
int yOff = oldMinY - minY;
int zOff = oldMinZ - minZ;
blit(memoryBlock.ptr(), 0, 0, 0, oldSizeX, oldSizeY, memoryBlock.ptr(), xOff, yOff, zOff, sizeX(), sizeY(), oldSizeX, oldSizeY, oldSizeZ);
}
public static void blit(long src, int srcX, int srcY, int srcZ, int srcSizeX, int srcSizeY, long dst, int dstX, int dstY, int dstZ, int dstSizeX, int dstSizeY, int sizeX, int sizeY, int sizeZ) {
for (int z = 0; z < sizeZ; z++) {
for (int y = 0; y < sizeY; y++) {
for (int x = 0; x < sizeX; x++) {
long srcPtr = src + offset(x + srcX, y + srcY, z + srcZ, srcSizeX, srcSizeY);
long dstPtr = dst + offset(x + dstX, y + dstY, z + dstZ, dstSizeX, dstSizeY);
MemoryUtil.memPutShort(dstPtr, MemoryUtil.memGetShort(srcPtr));
}
}
}
}
public static long offset(int x, int y, int z, int sizeX, int sizeY) {
return (x + sizeX * (y + sizeY * z)) * STRIDE;
}
public void clear() {
empty = true;
}
public void delete() {
if (memoryBlock != null) {
memoryBlock.free();
memoryBlock = null;
}
}
public long ptr() {
return memoryBlock.ptr();
}
public int x() {
return minX;
}
public int y() {
return minY;
}
public int z() {
return minZ;
}
public int sizeX() {
return maxX - minX;
}
public int sizeY() {
return maxY - minY;
}
public int sizeZ() {
return maxZ - minZ;
}
public int volume() {
return sizeX() * sizeY() * sizeZ();
}
}

View File

@ -1,11 +1,13 @@
package dev.engine_room.flywheel.backend.engine.embed;
public class EmbeddingUniforms {
public final class EmbeddingUniforms {
/**
* Only used by cull shaders.
*/
public static final String USE_MODEL_MATRIX = "_flw_useModelMatrix";
public static final String MODEL_MATRIX = "_flw_modelMatrix";
public static final String NORMAL_MATRIX = "_flw_normalMatrix";
public static final String USE_MODEL_MATRIX = "_flw_useModelMatrix";
public static final String MODEL_MATRIX1 = "_flw_modelMatrix";
public static final String ONE_OVER_LIGHT_BOX_SIZE = "_flw_oneOverLightBoxSize";
public static final String LIGHT_VOLUME_MIN = "_flw_lightVolumeMin";
public static final String USE_LIGHT_VOLUME = "_flw_useLightVolume";
private EmbeddingUniforms() {
}
}

View File

@ -6,11 +6,7 @@ import dev.engine_room.flywheel.backend.gl.shader.GlProgram;
public interface Environment {
ContextShader contextShader();
void setupDraw(GlProgram drawProgram);
void setupCull(GlProgram cullProgram);
void acquire();
void release();
void setupDraw(GlProgram drawProgram);
}

View File

@ -0,0 +1,18 @@
package dev.engine_room.flywheel.backend.engine.embed;
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
import it.unimi.dsi.fastutil.objects.ReferenceSet;
import it.unimi.dsi.fastutil.objects.ReferenceSets;
public class EnvironmentStorage {
protected final ReferenceSet<EmbeddedEnvironment> environments = ReferenceSets.synchronize(new ReferenceLinkedOpenHashSet<>());
public void track(EmbeddedEnvironment environment) {
environments.add(environment);
}
public void flush() {
environments.removeIf(EmbeddedEnvironment::isDeleted);
environments.forEach(EmbeddedEnvironment::flush);
}
}

View File

@ -14,23 +14,12 @@ public class GlobalEnvironment implements Environment {
return ContextShader.DEFAULT;
}
@Override
public void setupDraw(GlProgram drawProgram) {
}
@Override
public void setupCull(GlProgram cullProgram) {
cullProgram.setBool(EmbeddingUniforms.USE_MODEL_MATRIX, false);
}
@Override
public void acquire() {
}
@Override
public void release() {
public void setupDraw(GlProgram drawProgram) {
}
}

View File

@ -1,44 +0,0 @@
package dev.engine_room.flywheel.backend.engine.embed;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import dev.engine_room.flywheel.api.event.RenderStage;
import dev.engine_room.flywheel.backend.engine.EngineImpl;
import dev.engine_room.flywheel.backend.gl.shader.GlProgram;
import net.minecraft.world.level.BlockAndTintGetter;
public class NestedEmbeddedEnvironment extends AbstractEmbeddedEnvironment {
private final AbstractEmbeddedEnvironment parent;
public NestedEmbeddedEnvironment(AbstractEmbeddedEnvironment parent, EngineImpl engine, RenderStage renderStage) {
super(engine, renderStage);
this.parent = parent;
parent.acquire();
}
@Override
public void collectLight(BlockAndTintGetter level, int minX, int minY, int minZ, int sizeX, int sizeY, int sizeZ) {
}
@Override
public void invalidateLight() {
}
@Override
public void setupLight(GlProgram program) {
parent.setupLight(program);
}
@Override
public void composeMatrices(Matrix4f pose, Matrix3f normal) {
parent.composeMatrices(pose, normal);
pose.mul(this.pose);
normal.mul(this.normal);
}
@Override
public void actuallyDelete() {
parent.release();
}
}

View File

@ -1,79 +0,0 @@
package dev.engine_room.flywheel.backend.engine.embed;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import dev.engine_room.flywheel.api.event.RenderStage;
import dev.engine_room.flywheel.backend.Samplers;
import dev.engine_room.flywheel.backend.engine.EngineImpl;
import dev.engine_room.flywheel.backend.gl.shader.GlProgram;
import net.minecraft.world.level.BlockAndTintGetter;
public class TopLevelEmbeddedEnvironment extends AbstractEmbeddedEnvironment {
private final EmbeddedLightVolume lightVolume = new EmbeddedLightVolume();
private final EmbeddedLightTexture lightTexture = new EmbeddedLightTexture();
public TopLevelEmbeddedEnvironment(EngineImpl engine, RenderStage renderStage) {
super(engine, renderStage);
}
@Override
public void flush() {
super.flush();
if (lightVolume.empty()) {
return;
}
Samplers.EMBEDDED_LIGHT.makeActive();
lightTexture.bind();
lightTexture.ensureCapacity(lightVolume.sizeX(), lightVolume.sizeY(), lightVolume.sizeZ());
lightTexture.upload(lightVolume.ptr(), lightVolume.sizeX(), lightVolume.sizeY(), lightVolume.sizeZ());
}
@Override
public void collectLight(BlockAndTintGetter level, int minX, int minY, int minZ, int sizeX, int sizeY, int sizeZ) {
lightVolume.collect(level, minX, minY, minZ, sizeX, sizeY, sizeZ);
}
@Override
public void invalidateLight() {
lightVolume.clear();
}
@Override
public void setupLight(GlProgram program) {
if (!lightVolume.empty()) {
Samplers.EMBEDDED_LIGHT.makeActive();
lightTexture.bind();
float oneOverSizeX = 1f / (float) lightTexture.sizeX;
float oneOverSizeY = 1f / (float) lightTexture.sizeY;
float oneOverSizeZ = 1f / (float) lightTexture.sizeZ;
program.setVec3(EmbeddingUniforms.ONE_OVER_LIGHT_BOX_SIZE, oneOverSizeX, oneOverSizeY, oneOverSizeZ);
program.setVec3(EmbeddingUniforms.LIGHT_VOLUME_MIN, lightVolume.x(), lightVolume.y(), lightVolume.z());
program.setBool(EmbeddingUniforms.USE_LIGHT_VOLUME, true);
} else {
program.setBool(EmbeddingUniforms.USE_LIGHT_VOLUME, false);
}
}
@Override
public void composeMatrices(Matrix4f pose, Matrix3f normal) {
pose.set(this.pose);
normal.set(this.normal);
}
@Override
public void actuallyDelete() {
// We could technically free the light volume right away in _delete, but
// the control flow here is so convoluted that it's probably best to do
// everything in one place.
lightVolume.delete();
lightTexture.delete();
}
}

View File

@ -0,0 +1,14 @@
package dev.engine_room.flywheel.backend.engine.indirect;
public final class BufferBindings {
public static final int INSTANCE = 0;
public static final int TARGET = 1;
public static final int MODEL_INDEX = 2;
public static final int MODEL = 3;
public static final int DRAW = 4;
public static final int LIGHT_LUT = 5;
public static final int LIGHT_SECTION = 6;
private BufferBindings() {
}
}

View File

@ -22,13 +22,6 @@ public class IndirectBuffers {
public static final long DRAW_COMMAND_STRIDE = 40;
public static final long DRAW_COMMAND_OFFSET = 0;
public static final int INSTANCE_INDEX = 0;
public static final int TARGET_INDEX = 1;
public static final int MODEL_INDEX_INDEX = 2;
public static final int MODEL_INDEX = 3;
public static final int DRAW_INDEX = 4;
// Offsets to the 3 segments
private static final long HANDLE_OFFSET = 0;
private static final long OFFSET_OFFSET = BUFFER_COUNT * INT_SIZE;
@ -117,7 +110,7 @@ public class IndirectBuffers {
private void multiBind() {
final long ptr = multiBindBlock.ptr();
nglBindBuffersRange(GL_SHADER_STORAGE_BUFFER, 0, IndirectBuffers.BUFFER_COUNT, ptr, ptr + OFFSET_OFFSET, ptr + SIZE_OFFSET);
nglBindBuffersRange(GL_SHADER_STORAGE_BUFFER, BufferBindings.INSTANCE, IndirectBuffers.BUFFER_COUNT, ptr, ptr + OFFSET_OFFSET, ptr + SIZE_OFFSET);
}
/**
@ -125,7 +118,7 @@ public class IndirectBuffers {
*/
public void bindForCrumbling() {
final long ptr = multiBindBlock.ptr();
nglBindBuffersRange(GL_SHADER_STORAGE_BUFFER, 0, 4, ptr, ptr + OFFSET_OFFSET, ptr + SIZE_OFFSET);
nglBindBuffersRange(GL_SHADER_STORAGE_BUFFER, BufferBindings.INSTANCE, 4, ptr, ptr + OFFSET_OFFSET, ptr + SIZE_OFFSET);
}
public void delete() {

View File

@ -3,12 +3,10 @@ package dev.engine_room.flywheel.backend.engine.indirect;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
import static org.lwjgl.opengl.GL30.glUniform1ui;
import static org.lwjgl.opengl.GL40.glDrawElementsIndirect;
import static org.lwjgl.opengl.GL42.GL_COMMAND_BARRIER_BIT;
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT;
import static org.lwjgl.opengl.GL43.glDispatchCompute;
import static org.lwjgl.opengl.GL43.glMultiDrawElementsIndirect;
import java.util.ArrayList;
import java.util.Comparator;
@ -27,7 +25,6 @@ import dev.engine_room.flywheel.backend.engine.MaterialRenderState;
import dev.engine_room.flywheel.backend.engine.MeshPool;
import dev.engine_room.flywheel.backend.engine.embed.Environment;
import dev.engine_room.flywheel.backend.engine.uniform.Uniforms;
import dev.engine_room.flywheel.backend.gl.Driver;
import dev.engine_room.flywheel.backend.gl.GlCompat;
import dev.engine_room.flywheel.backend.gl.shader.GlProgram;
import dev.engine_room.flywheel.lib.math.MoreMath;
@ -280,16 +277,19 @@ public class IndirectCullingGroup<I extends Instance> {
buffers.delete();
}
public boolean checkEmptyAndDelete() {
var out = indirectDraws.isEmpty();
if (out) {
delete();
}
return out;
}
private record MultiDraw(Material material, int start, int end) {
private void submit() {
if (GlCompat.DRIVER == Driver.INTEL) {
// Intel renders garbage with MDI, but Consecutive Normal Draws works fine.
for (int i = this.start; i < this.end; i++) {
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, i * IndirectBuffers.DRAW_COMMAND_STRIDE);
}
} else {
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, this.start * IndirectBuffers.DRAW_COMMAND_STRIDE, this.end - this.start, (int) IndirectBuffers.DRAW_COMMAND_STRIDE);
}
GlCompat.safeMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, this.start * IndirectBuffers.DRAW_COMMAND_STRIDE, this.end - this.start, (int) IndirectBuffers.DRAW_COMMAND_STRIDE);
}
}
}

View File

@ -20,6 +20,7 @@ import dev.engine_room.flywheel.backend.engine.CommonCrumbling;
import dev.engine_room.flywheel.backend.engine.DrawManager;
import dev.engine_room.flywheel.backend.engine.GroupKey;
import dev.engine_room.flywheel.backend.engine.InstancerKey;
import dev.engine_room.flywheel.backend.engine.LightStorage;
import dev.engine_room.flywheel.backend.engine.MaterialRenderState;
import dev.engine_room.flywheel.backend.engine.MeshPool;
import dev.engine_room.flywheel.backend.engine.TextureBinder;
@ -39,17 +40,17 @@ public class IndirectDrawManager extends DrawManager<IndirectInstancer<?>> {
private final GlVertexArray vertexArray;
private final Map<GroupKey<?>, IndirectCullingGroup<?>> cullingGroups = new HashMap<>();
private final GlBuffer crumblingDrawBuffer = new GlBuffer();
private final LightBuffers lightBuffers;
public IndirectDrawManager(IndirectPrograms programs) {
this.programs = programs;
programs.acquire();
stagingBuffer = new StagingBuffer(this.programs);
meshPool = new MeshPool();
vertexArray = GlVertexArray.create();
meshPool.bind(vertexArray);
lightBuffers = new LightBuffers();
}
@Override
@ -83,6 +84,7 @@ public class IndirectDrawManager extends DrawManager<IndirectInstancer<?>> {
TextureBinder.bindLightAndOverlay();
vertexArray.bindForDraw();
lightBuffers.bind();
Uniforms.bindAll();
for (var group : cullingGroups.values()) {
@ -95,19 +97,25 @@ public class IndirectDrawManager extends DrawManager<IndirectInstancer<?>> {
}
@Override
public void flush() {
super.flush();
public void flush(LightStorage lightStorage) {
super.flush(lightStorage);
for (var group : cullingGroups.values()) {
group.flushInstancers();
}
instancers.values().removeIf(instancer -> instancer.instanceCount() == 0);
cullingGroups.values()
.removeIf(IndirectCullingGroup::checkEmptyAndDelete);
instancers.values()
.removeIf(instancer -> instancer.instanceCount() == 0);
meshPool.flush();
stagingBuffer.reclaim();
lightBuffers.flush(stagingBuffer, lightStorage);
for (var group : cullingGroups.values()) {
group.upload(stagingBuffer);
}
@ -159,7 +167,7 @@ public class IndirectDrawManager extends DrawManager<IndirectInstancer<?>> {
var block = MemoryBlock.malloc(IndirectBuffers.DRAW_COMMAND_STRIDE);
GlBufferType.DRAW_INDIRECT_BUFFER.bind(crumblingDrawBuffer.handle());
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, IndirectBuffers.DRAW_INDEX, crumblingDrawBuffer.handle(), 0, IndirectBuffers.DRAW_COMMAND_STRIDE);
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, BufferBindings.DRAW, crumblingDrawBuffer.handle(), 0, IndirectBuffers.DRAW_COMMAND_STRIDE);
for (var groupEntry : byType.entrySet()) {
var byProgress = groupEntry.getValue();

View File

@ -125,8 +125,6 @@ public class IndirectInstancer<I extends Instance> extends AbstractInstancer<I>
@Override
public void delete() {
super.delete();
for (IndirectDraw draw : draws()) {
draw.delete();
}

View File

@ -0,0 +1,43 @@
package dev.engine_room.flywheel.backend.engine.indirect;
import org.lwjgl.opengl.GL46;
import org.lwjgl.system.MemoryUtil;
import dev.engine_room.flywheel.backend.engine.LightStorage;
public class LightBuffers {
private final ResizableStorageArray lut = new ResizableStorageArray(4);
private final ResizableStorageArray sections = new ResizableStorageArray(LightStorage.SECTION_SIZE_BYTES);
public void flush(StagingBuffer staging, LightStorage light) {
var capacity = light.capacity();
if (capacity == 0) {
return;
}
sections.ensureCapacity(capacity);
light.uploadChangedSections(staging, sections.handle());
if (light.checkNeedsLutRebuildAndClear()) {
var lut = light.createLut();
this.lut.ensureCapacity(lut.size());
staging.enqueueCopy((long) lut.size() * Integer.BYTES, this.lut.handle(), 0, ptr -> {
for (int i = 0; i < lut.size(); i++) {
MemoryUtil.memPutInt(ptr + (long) i * Integer.BYTES, lut.getInt(i));
}
});
}
}
public void bind() {
if (sections.capacity() == 0) {
return;
}
GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.LIGHT_LUT, lut.handle(), 0, lut.byteCapacity());
GL46.glBindBufferRange(GL46.GL_SHADER_STORAGE_BUFFER, BufferBindings.LIGHT_SECTION, sections.handle(), 0, sections.byteCapacity());
}
}

View File

@ -18,6 +18,7 @@ import dev.engine_room.flywheel.backend.engine.CommonCrumbling;
import dev.engine_room.flywheel.backend.engine.DrawManager;
import dev.engine_room.flywheel.backend.engine.GroupKey;
import dev.engine_room.flywheel.backend.engine.InstancerKey;
import dev.engine_room.flywheel.backend.engine.LightStorage;
import dev.engine_room.flywheel.backend.engine.MaterialEncoder;
import dev.engine_room.flywheel.backend.engine.MaterialRenderState;
import dev.engine_room.flywheel.backend.engine.MeshPool;
@ -42,6 +43,7 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
private final MeshPool meshPool;
private final GlVertexArray vao;
private final TextureBuffer instanceTexture;
private final InstancedLight light;
public InstancedDrawManager(InstancingPrograms programs) {
programs.acquire();
@ -50,16 +52,17 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
meshPool = new MeshPool();
vao = GlVertexArray.create();
instanceTexture = new TextureBuffer();
light = new InstancedLight();
meshPool.bind(vao);
}
@Override
public void flush() {
super.flush();
public void flush(LightStorage lightStorage) {
super.flush(lightStorage);
var instancers = this.instancers.values();
instancers.removeIf(instancer -> {
this.instancers.values()
.removeIf(instancer -> {
// Update the instancers and remove any that are empty.
instancer.update();
@ -77,6 +80,8 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
}
meshPool.flush();
light.flush(lightStorage);
}
@Override
@ -91,6 +96,7 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
Uniforms.bindAll();
vao.bindForDraw();
TextureBinder.bindLightAndOverlay();
light.bind();
drawSet.draw(instanceTexture, programs);
@ -113,6 +119,8 @@ public class InstancedDrawManager extends DrawManager<InstancedInstancer<?>> {
programs.release();
vao.delete();
light.delete();
super.delete();
}

View File

@ -116,8 +116,6 @@ public class InstancedInstancer<I extends Instance> extends AbstractInstancer<I>
}
public void delete() {
super.delete();
if (vbo == null) {
return;
}

View File

@ -0,0 +1,62 @@
package dev.engine_room.flywheel.backend.engine.instancing;
import org.lwjgl.opengl.GL32;
import org.lwjgl.system.MemoryUtil;
import dev.engine_room.flywheel.backend.Samplers;
import dev.engine_room.flywheel.backend.engine.LightStorage;
import dev.engine_room.flywheel.backend.gl.TextureBuffer;
import dev.engine_room.flywheel.backend.gl.buffer.GlBuffer;
import dev.engine_room.flywheel.lib.memory.MemoryBlock;
public class InstancedLight {
private final GlBuffer lut;
private final GlBuffer sections;
private final TextureBuffer lutTexture;
private final TextureBuffer sectionsTexture;
public InstancedLight() {
lut = new GlBuffer();
sections = new GlBuffer();
lutTexture = new TextureBuffer(GL32.GL_R32UI);
sectionsTexture = new TextureBuffer(GL32.GL_R32UI);
}
public void bind() {
Samplers.LIGHT_LUT.makeActive();
lutTexture.bind(lut.handle());
Samplers.LIGHT_SECTIONS.makeActive();
sectionsTexture.bind(sections.handle());
}
public void flush(LightStorage light) {
if (light.capacity() == 0) {
return;
}
light.upload(sections);
if (light.checkNeedsLutRebuildAndClear()) {
var lut = light.createLut();
var up = MemoryBlock.malloc((long) lut.size() * Integer.BYTES);
long ptr = up.ptr();
for (int i = 0; i < lut.size(); i++) {
MemoryUtil.memPutInt(ptr + (long) Integer.BYTES * i, lut.getInt(i));
}
this.lut.upload(up);
up.free();
}
}
public void delete() {
lut.delete();
sections.delete();
lutTexture.delete();
sectionsTexture.delete();
}
}

View File

@ -14,7 +14,6 @@ public enum DebugMode implements StringRepresentable {
LIGHT_COLOR,
OVERLAY,
DIFFUSE,
LIGHT_VOLUME,
;
public static final Codec<DebugMode> CODEC = StringRepresentable.fromEnum(DebugMode::values);

View File

@ -8,6 +8,8 @@ import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL20C;
import org.lwjgl.opengl.GL31C;
import org.lwjgl.opengl.GL40;
import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.opengl.KHRShaderSubgroup;
import org.lwjgl.system.MemoryStack;
@ -71,6 +73,23 @@ public final class GlCompat {
}
}
/**
* Similar in function to {@link GL43#glMultiDrawElementsIndirect(int, int, long, int, int)},
* but uses consecutive DI instead of MDI if MDI is known to not work well with the current driver.
* Unlike the original function, stride cannot be equal to 0.
*/
public static void safeMultiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride) {
if (GlCompat.DRIVER == Driver.INTEL) {
// Intel renders garbage with MDI, but consecutive DI works fine.
for (int i = 0; i < drawcount; i++) {
GL40.glDrawElementsIndirect(mode, type, indirect);
indirect += stride;
}
} else {
GL43.glMultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
}
private static Driver readVendorString() {
if (CAPABILITIES == null) {
return Driver.UNKNOWN;

View File

@ -5,14 +5,20 @@ import org.lwjgl.opengl.GL32;
public class TextureBuffer extends GlObject {
public static final int MAX_TEXELS = GL32.glGetInteger(GL32.GL_MAX_TEXTURE_BUFFER_SIZE);
public static final int MAX_BYTES = MAX_TEXELS * 16; // 4 channels * 4 bytes
private final int format;
public TextureBuffer() {
this(GL32.GL_RGBA32UI);
}
public TextureBuffer(int format) {
handle(GL32.glGenTextures());
this.format = format;
}
public void bind(int buffer) {
GL32.glBindTexture(GL32.GL_TEXTURE_BUFFER, handle());
GL32.glTexBuffer(GL32.GL_TEXTURE_BUFFER, GL32.GL_RGBA32UI, buffer);
GL32.glTexBuffer(GL32.GL_TEXTURE_BUFFER, format, buffer);
}
@Override

View File

@ -0,0 +1,29 @@
package dev.engine_room.flywheel.backend.mixin;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.engine_room.flywheel.backend.LightUpdateHolder;
import net.minecraft.client.multiplayer.ClientChunkCache;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.LightLayer;
@Mixin(ClientChunkCache.class)
abstract class ClientChunkCacheMixin {
@Shadow
@Final
ClientLevel level;
@Inject(method = "onLightUpdate", at = @At("HEAD"))
private void flywheel$backend$onLightUpdate(LightLayer layer, SectionPos pos, CallbackInfo ci) {
// This is duplicated from code in impl, but I'm not sure that it
// makes sense to be generically passed to backends.
LightUpdateHolder.get(level)
.add(pos.asLong());
}
}

View File

@ -1,4 +1,5 @@
#include "flywheel:internal/material.glsl"
#include "flywheel:internal/api_impl.glsl"
#include "flywheel:internal/uniforms/uniforms.glsl"
in vec4 flw_vertexPos;

View File

@ -0,0 +1,13 @@
// TODO: Add config for light smoothness. Should work at a compile flag level
/// Get the light at the given world position from the given normal.
/// This may be interpolated for smooth lighting.
bool flw_light(vec3 worldPos, vec3 normal, out vec2 light);
/// Get the light at the given world position.
/// This may be interpolated for smooth lighting.
bool flw_light(vec3 worldPos, out vec2 light);
/// Fetches the light value at the given block position.
/// Returns false if the light for the given block is not available.
bool flw_lightFetch(ivec3 blockPos, out vec2 light);

View File

@ -1,4 +1,5 @@
#include "flywheel:internal/material.glsl"
#include "flywheel:internal/api_impl.glsl"
#include "flywheel:internal/uniforms/uniforms.glsl"
out vec4 flw_vertexPos;

View File

@ -13,14 +13,6 @@ uniform sampler2D _flw_crumblingTex;
in vec2 _flw_crumblingTexCoord;
#endif
#ifdef _FLW_EMBEDDED
uniform sampler3D _flw_lightVolume;
uniform bool _flw_useLightVolume;
in vec3 _flw_lightVolumeCoord;
#endif
flat in uint _flw_instanceID;
out vec4 _flw_outputColor;
@ -43,12 +35,6 @@ void _flw_main() {
flw_fragOverlay = flw_vertexOverlay;
flw_fragLight = flw_vertexLight;
#ifdef _FLW_EMBEDDED
if (_flw_useLightVolume) {
flw_fragLight = max(flw_fragLight, texture(_flw_lightVolume, _flw_lightVolumeCoord).rg);
}
#endif
flw_materialFragment();
#ifdef _FLW_CRUMBLING
@ -98,11 +84,6 @@ void _flw_main() {
case 6u:
color = vec4(vec3(diffuseFactor), 1.);
break;
#ifdef _FLW_EMBEDDED
case 7u:
color = vec4(_flw_lightVolumeCoord, 1.);
break;
#endif
}
_flw_outputColor = flw_fogFilter(color);

View File

@ -66,13 +66,9 @@ vec2 getCrumblingTexCoord() {
}
#endif
#ifdef _FLW_EMBEDDED
uniform vec3 _flw_oneOverLightBoxSize;
uniform vec3 _flw_lightVolumeMin;
#ifdef FLW_EMBEDDED
uniform mat4 _flw_modelMatrix;
uniform mat3 _flw_normalMatrix;
out vec3 _flw_lightVolumeCoord;
#endif
flat out uint _flw_instanceID;
@ -86,11 +82,9 @@ void _flw_main(in FlwInstance instance, in uint stableInstanceID) {
_flw_crumblingTexCoord = getCrumblingTexCoord();
#endif
#ifdef _FLW_EMBEDDED
#ifdef FLW_EMBEDDED
flw_vertexPos = _flw_modelMatrix * flw_vertexPos;
flw_vertexNormal = _flw_normalMatrix * flw_vertexNormal;
_flw_lightVolumeCoord = (flw_vertexPos.xyz - _flw_lightVolumeMin) * _flw_oneOverLightBoxSize;
#endif
flw_vertexNormal = normalize(flw_vertexNormal);

View File

@ -3,3 +3,5 @@
#define _FLW_MODEL_INDEX_BUFFER_BINDING 2
#define _FLW_MODEL_BUFFER_BINDING 3
#define _FLW_DRAW_BUFFER_BINDING 4
#define _FLW_LIGHT_LUT_BUFFER_BINDING 5
#define _FLW_LIGHT_SECTIONS_BUFFER_BINDING 6

View File

@ -0,0 +1,17 @@
#include "flywheel:internal/light_lut.glsl"
layout(std430, binding = _FLW_LIGHT_LUT_BUFFER_BINDING) restrict readonly buffer LightLut {
uint _flw_lightLut[];
};
layout(std430, binding = _FLW_LIGHT_SECTIONS_BUFFER_BINDING) restrict readonly buffer LightSections {
uint _flw_lightSections[];
};
uint _flw_indexLut(uint index) {
return _flw_lightLut[index];
}
uint _flw_indexLight(uint index) {
return _flw_lightSections[index];
}

View File

@ -1,4 +1,6 @@
#include "flywheel:internal/common.frag"
#include "flywheel:internal/indirect/buffer_bindings.glsl"
#include "flywheel:internal/indirect/light.glsl"
flat in uvec3 _flw_packedMaterial;

View File

@ -2,6 +2,7 @@
#include "flywheel:internal/packed_material.glsl"
#include "flywheel:internal/indirect/buffer_bindings.glsl"
#include "flywheel:internal/indirect/draw_command.glsl"
#include "flywheel:internal/indirect/light.glsl"
layout(std430, binding = _FLW_TARGET_BUFFER_BINDING) restrict readonly buffer TargetBuffer {
uint _flw_instanceIndices[];

View File

@ -0,0 +1,12 @@
#include "flywheel:internal/light_lut.glsl"
uniform usamplerBuffer _flw_lightLut;
uniform usamplerBuffer _flw_lightSections;
uint _flw_indexLut(uint index) {
return texelFetch(_flw_lightLut, int(index)).r;
}
uint _flw_indexLight(uint index) {
return texelFetch(_flw_lightSections, int(index)).r;
}

View File

@ -1,4 +1,5 @@
#include "flywheel:internal/common.frag"
#include "flywheel:internal/instancing/light.glsl"
uniform uvec4 _flw_packedMaterial;

View File

@ -1,5 +1,6 @@
#include "flywheel:internal/common.vert"
#include "flywheel:internal/packed_material.glsl"
#include "flywheel:internal/instancing/light.glsl"
uniform uvec4 _flw_packedMaterial;
uniform int _flw_baseInstance = 0;

View File

@ -0,0 +1,235 @@
const uint _FLW_LIGHT_SECTION_SIZE_BYTES = 18 * 18 * 18;
const uint _FLW_LIGHT_SECTION_SIZE_INTS = _FLW_LIGHT_SECTION_SIZE_BYTES / 4;
uint _flw_indexLut(uint index);
uint _flw_indexLight(uint index);
/// Find the index for the next step in the LUT.
/// @param base The base index in the LUT, should point to the start of a coordinate span.
/// @param coord The coordinate to look for.
/// @param next Output. The index of the next step in the LUT.
/// @return true if the coordinate is not in the span.
bool _flw_nextLut(uint base, int coord, out uint next) {
// The base coordinate.
int start = int(_flw_indexLut(base));
// The width of the coordinate span.
uint size = _flw_indexLut(base + 1);
// Index of the coordinate in the span.
int i = coord - start;
if (i < 0 || i >= size) {
// We missed.
return true;
}
next = _flw_indexLut(base + 2 + i);
return false;
}
bool _flw_chunkCoordToSectionIndex(ivec3 sectionPos, out uint index) {
uint y;
if (_flw_nextLut(0, sectionPos.x, y) || y == 0) {
return true;
}
uint z;
if (_flw_nextLut(y, sectionPos.y, z) || z == 0) {
return true;
}
uint sectionIndex;
if (_flw_nextLut(z, sectionPos.z, sectionIndex) || sectionIndex == 0) {
return true;
}
// The index is written as 1-based so we can properly detect missing sections.
index = sectionIndex - 1;
return false;
}
vec2 _flw_lightAt(uint sectionOffset, uvec3 blockInSectionPos) {
uint byteOffset = blockInSectionPos.x + blockInSectionPos.z * 18u + blockInSectionPos.y * 18u * 18u;
uint uintOffset = byteOffset >> 2u;
uint bitOffset = (byteOffset & 3u) << 3;
uint raw = _flw_indexLight(sectionOffset + uintOffset);
uint block = (raw >> bitOffset) & 0xFu;
uint sky = (raw >> (bitOffset + 4u)) & 0xFu;
return vec2(block, sky);
}
bool flw_lightFetch(ivec3 blockPos, out vec2 lightCoord) {
uint lightSectionIndex;
if (_flw_chunkCoordToSectionIndex(blockPos >> 4, lightSectionIndex)) {
return false;
}
// The offset of the section in the light buffer.
uint sectionOffset = lightSectionIndex * _FLW_LIGHT_SECTION_SIZE_INTS;
uvec3 blockInSectionPos = (blockPos & 0xF) + 1;
lightCoord = _flw_lightAt(sectionOffset, blockInSectionPos) / 15.;
return true;
}
bool flw_light(vec3 worldPos, out vec2 lightCoord) {
// Always use the section of the block we are contained in to ensure accuracy.
// We don't want to interpolate between sections, but also we might not be able
// to rely on the existence neighboring sections, so don't do any extra rounding here.
ivec3 blockPos = ivec3(floor(worldPos));
uint lightSectionIndex;
if (_flw_chunkCoordToSectionIndex(blockPos >> 4, lightSectionIndex)) {
return false;
}
// The offset of the section in the light buffer.
uint sectionOffset = lightSectionIndex * _FLW_LIGHT_SECTION_SIZE_INTS;
// The block's position in the section adjusted into 18x18x18 space
uvec3 blockInSectionPos = (blockPos & 0xF) + 1;
// The lowest corner of the 2x2x2 area we'll be trilinear interpolating.
// The ugly bit on the end evaluates to -1 or 0 depending on which side of 0.5 we are.
uvec3 lowestCorner = blockInSectionPos + ivec3(floor(fract(worldPos) - 0.5));
// The distance our fragment is from the center of the lowest corner.
vec3 interpolant = fract(worldPos - 0.5);
// Fetch everything for trilinear interpolation
// Hypothetically we could re-order these and do some calculations in-between fetches
// to help with latency hiding, but the compiler should be able to do that for us.
vec2 light000 = _flw_lightAt(sectionOffset, lowestCorner);
vec2 light001 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 0, 1));
vec2 light010 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 0));
vec2 light011 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(0, 1, 1));
vec2 light100 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 0));
vec2 light101 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 0, 1));
vec2 light110 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 0));
vec2 light111 = _flw_lightAt(sectionOffset, lowestCorner + uvec3(1, 1, 1));
vec2 light00 = mix(light000, light001, interpolant.z);
vec2 light01 = mix(light010, light011, interpolant.z);
vec2 light10 = mix(light100, light101, interpolant.z);
vec2 light11 = mix(light110, light111, interpolant.z);
vec2 light0 = mix(light00, light01, interpolant.y);
vec2 light1 = mix(light10, light11, interpolant.y);
lightCoord = mix(light0, light1, interpolant.x) / 15.;
return true;
}
uint _flw_lightIndex(in uvec3 p) {
return p.x + p.z * 3u + p.y * 9u;
}
/// Premtively collect all light in a 3x3x3 area centered on our block.
/// Depending on the normal, we won't use all the data, but fetching on demand will have many duplicated fetches.
vec2[27] _flw_lightFetch3x3x3(uint sectionOffset, ivec3 blockInSectionPos) {
vec2[27] lights;
for (int y = -1; y <= 1; y++) {
for (int z = -1; z <= 1; z++) {
for (int x = -1; x <= 1; x++) {
lights[_flw_lightIndex(uvec3(x + 1, y + 1, z + 1))] = _flw_lightAt(sectionOffset, uvec3(blockInSectionPos + ivec3(x, y, z)));
}
}
}
return lights;
}
/// Calculate the light for a direction by averaging the light at the corners of the block.
///
/// To make this reusable across directions, c00..c11 choose what values relative to each corner to use.
/// e.g. (0, 0, 0) (0, 0, 1) (0, 1, 0) (0, 1, 1) would give you the light coming from -x at each corner.
/// In general, to get the light for a particular direction, you fix the x, y, or z coordinate of the c values, and permutate 0 and 1 for the other two.
/// Fixing the x coordinate to 0 gives you the light from -x, 1 gives you the light from +x.
///
/// @param lights The light data for the 3x3x3 area.
/// @param interpolant The position within the center block.
/// @param c00..c11 4 offsets to determine which "direction" we are averaging.
vec2 _flw_lightForDirection(in vec2[27] lights, in vec3 interpolant, in uvec3 c00, in uvec3 c01, in uvec3 c10, in uvec3 c11) {
vec2 light000 = lights[_flw_lightIndex(c00 + uvec3(0u, 0u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 0u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 0u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 0u, 0u))];
vec2 light001 = lights[_flw_lightIndex(c00 + uvec3(0u, 0u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 0u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 0u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 0u, 1u))];
vec2 light010 = lights[_flw_lightIndex(c00 + uvec3(0u, 1u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 1u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 1u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 1u, 0u))];
vec2 light011 = lights[_flw_lightIndex(c00 + uvec3(0u, 1u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(0u, 1u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(0u, 1u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(0u, 1u, 1u))];
vec2 light100 = lights[_flw_lightIndex(c00 + uvec3(1u, 0u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 0u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 0u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 0u, 0u))];
vec2 light101 = lights[_flw_lightIndex(c00 + uvec3(1u, 0u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 0u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 0u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 0u, 1u))];
vec2 light110 = lights[_flw_lightIndex(c00 + uvec3(1u, 1u, 0u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 1u, 0u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 1u, 0u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 1u, 0u))];
vec2 light111 = lights[_flw_lightIndex(c00 + uvec3(1u, 1u, 1u))] + lights[_flw_lightIndex(c01 + uvec3(1u, 1u, 1u))] + lights[_flw_lightIndex(c10 + uvec3(1u, 1u, 1u))] + lights[_flw_lightIndex(c11 + uvec3(1u, 1u, 1u))];
vec2 light00 = mix(light000, light001, interpolant.z);
vec2 light01 = mix(light010, light011, interpolant.z);
vec2 light10 = mix(light100, light101, interpolant.z);
vec2 light11 = mix(light110, light111, interpolant.z);
vec2 light0 = mix(light00, light01, interpolant.y);
vec2 light1 = mix(light10, light11, interpolant.y);
// Divide by 60 (15 * 4) to normalize.
return mix(light0, light1, interpolant.x) / 63.;
}
bool flw_light(vec3 worldPos, vec3 normal, out vec2 lightCoord) {
// Always use the section of the block we are contained in to ensure accuracy.
// We don't want to interpolate between sections, but also we might not be able
// to rely on the existence neighboring sections, so don't do any extra rounding here.
ivec3 blockPos = ivec3(floor(worldPos));
uint lightSectionIndex;
if (_flw_chunkCoordToSectionIndex(blockPos >> 4, lightSectionIndex)) {
return false;
}
// The offset of the section in the light buffer.
uint sectionOffset = lightSectionIndex * _FLW_LIGHT_SECTION_SIZE_INTS;
// The block's position in the section adjusted into 18x18x18 space
ivec3 blockInSectionPos = (blockPos & 0xF) + 1;
// Fetch everything in a 3x3x3 area centered around the block.
vec2[27] lights = _flw_lightFetch3x3x3(sectionOffset, blockInSectionPos);
vec3 interpolant = fract(worldPos);
vec2 lightX;
if (normal.x > 0) {
lightX = _flw_lightForDirection(lights, interpolant, uvec3(1u, 0u, 0u), uvec3(1u, 0u, 1u), uvec3(1u, 1u, 0u), uvec3(1u, 1u, 1u));
} else if (normal.x < 0) {
lightX = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 0u, 1u), uvec3(0u, 1u, 0u), uvec3(0u, 1u, 1u));
} else {
lightX = vec2(0.);
}
vec2 lightZ;
if (normal.z > 0) {
lightZ = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 1u), uvec3(0u, 1u, 1u), uvec3(1u, 0u, 1u), uvec3(1u, 1u, 1u));
} else if (normal.z < 0) {
lightZ = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 1u, 0u), uvec3(1u, 0u, 0u), uvec3(1u, 1u, 0u));
} else {
lightZ = vec2(0.);
}
vec2 lightY;
// Average the light in relevant directions at each corner.
if (normal.y > 0.) {
lightY = _flw_lightForDirection(lights, interpolant, uvec3(0u, 1u, 0u), uvec3(0u, 1u, 1u), uvec3(1u, 1u, 0u), uvec3(1u, 1u, 1u));
} else if (normal.y < 0.) {
lightY = _flw_lightForDirection(lights, interpolant, uvec3(0u, 0u, 0u), uvec3(0u, 0u, 1u), uvec3(1u, 0u, 0u), uvec3(1u, 0u, 1u));
} else {
lightY = vec2(0.);
}
vec3 n2 = normal * normal;
lightCoord = lightX * n2.x + lightY * n2.y + lightZ * n2.z;
return true;
}

View File

@ -6,6 +6,7 @@
"refmap": "backend-flywheel.refmap.json",
"client": [
"AbstractClientPlayerAccessor",
"ClientChunkCacheMixin",
"GlStateManagerMixin",
"LevelRendererAccessor",
"OptionsMixin",

View File

@ -1,142 +0,0 @@
package dev.engine_room.flywheel.lib.box;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.AABB;
public interface Box {
int getMinX();
int getMinY();
int getMinZ();
int getMaxX();
int getMaxY();
int getMaxZ();
default int sizeX() {
return getMaxX() - getMinX();
}
default int sizeY() {
return getMaxY() - getMinY();
}
default int sizeZ() {
return getMaxZ() - getMinZ();
}
default int volume() {
return sizeX() * sizeY() * sizeZ();
}
default boolean isEmpty() {
// if any dimension has side length 0 this box contains no volume
return getMinX() == getMaxX() || getMinY() == getMaxY() || getMinZ() == getMaxZ();
}
default boolean sameAs(Box other) {
return getMinX() == other.getMinX() && getMinY() == other.getMinY() && getMinZ() == other.getMinZ() && getMaxX() == other.getMaxX() && getMaxY() == other.getMaxY() && getMaxZ() == other.getMaxZ();
}
default boolean sameAs(Box other, int margin) {
return getMinX() == other.getMinX() - margin &&
getMinY() == other.getMinY() - margin &&
getMinZ() == other.getMinZ() - margin &&
getMaxX() == other.getMaxX() + margin &&
getMaxY() == other.getMaxY() + margin &&
getMaxZ() == other.getMaxZ() + margin;
}
default boolean sameAs(AABB other) {
return getMinX() == Math.floor(other.minX)
&& getMinY() == Math.floor(other.minY)
&& getMinZ() == Math.floor(other.minZ)
&& getMaxX() == Math.ceil(other.maxX)
&& getMaxY() == Math.ceil(other.maxY)
&& getMaxZ() == Math.ceil(other.maxZ);
}
default boolean intersects(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return this.getMinX() < maxX && this.getMaxX() > minX && this.getMinY() < maxY && this.getMaxY() > minY && this.getMinZ() < maxZ && this.getMaxZ() > minZ;
}
default boolean intersects(Box other) {
return this.intersects(other.getMinX(), other.getMinY(), other.getMinZ(), other.getMaxX(), other.getMaxY(), other.getMaxZ());
}
default boolean contains(int x, int y, int z) {
return x >= getMinX()
&& x <= getMaxX()
&& y >= getMinY()
&& y <= getMaxY()
&& z >= getMinZ()
&& z <= getMaxZ();
}
default boolean contains(Box other) {
return other.getMinX() >= this.getMinX()
&& other.getMaxX() <= this.getMaxX()
&& other.getMinY() >= this.getMinY()
&& other.getMaxY() <= this.getMaxY()
&& other.getMinZ() >= this.getMinZ()
&& other.getMaxZ() <= this.getMaxZ();
}
default void forEachContained(CoordinateConsumer func) {
int minX = getMinX();
int minY = getMinY();
int minZ = getMinZ();
int maxX = getMaxX();
int maxY = getMaxY();
int maxZ = getMaxZ();
for (int x = minX; x < maxX; x++) {
for (int y = minY; y < maxY; y++) {
for (int z = minZ; z < maxZ; z++) {
func.accept(x, y, z);
}
}
}
}
default boolean hasPowerOf2Sides() {
// this is only true if all individual side lengths are powers of 2
return Mth.isPowerOfTwo(volume());
}
default MutableBox union(Box other) {
int minX = Math.min(this.getMinX(), other.getMinX());
int minY = Math.min(this.getMinY(), other.getMinY());
int minZ = Math.min(this.getMinZ(), other.getMinZ());
int maxX = Math.max(this.getMaxX(), other.getMaxX());
int maxY = Math.max(this.getMaxY(), other.getMaxY());
int maxZ = Math.max(this.getMaxZ(), other.getMaxZ());
return new MutableBox(minX, minY, minZ, maxX, maxY, maxZ);
}
default MutableBox intersect(Box other) {
int minX = Math.max(this.getMinX(), other.getMinX());
int minY = Math.max(this.getMinY(), other.getMinY());
int minZ = Math.max(this.getMinZ(), other.getMinZ());
int maxX = Math.min(this.getMaxX(), other.getMaxX());
int maxY = Math.min(this.getMaxY(), other.getMaxY());
int maxZ = Math.min(this.getMaxZ(), other.getMaxZ());
return new MutableBox(minX, minY, minZ, maxX, maxY, maxZ);
}
default AABB toAABB() {
return new AABB(getMinX(), getMinY(), getMinZ(), getMaxX(), getMaxY(), getMaxZ());
}
default MutableBox copy() {
return new MutableBox(getMinX(), getMinY(), getMinZ(), getMaxX(), getMaxY(), getMaxZ());
}
@FunctionalInterface
interface CoordinateConsumer {
void accept(int x, int y, int z);
}
}

View File

@ -1,329 +0,0 @@
package dev.engine_room.flywheel.lib.box;
import java.util.Collection;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.SectionPos;
import net.minecraft.core.Vec3i;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.AABB;
public class MutableBox implements Box {
protected int minX;
protected int minY;
protected int minZ;
protected int maxX;
protected int maxY;
protected int maxZ;
public MutableBox() {
}
public MutableBox(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
this.minX = minX;
this.minY = minY;
this.minZ = minZ;
this.maxX = maxX;
this.maxY = maxY;
this.maxZ = maxZ;
}
public static MutableBox from(AABB aabb) {
int minX = (int) Math.floor(aabb.minX);
int minY = (int) Math.floor(aabb.minY);
int minZ = (int) Math.floor(aabb.minZ);
int maxX = (int) Math.ceil(aabb.maxX);
int maxY = (int) Math.ceil(aabb.maxY);
int maxZ = (int) Math.ceil(aabb.maxZ);
return new MutableBox(minX, minY, minZ, maxX, maxY, maxZ);
}
public static MutableBox from(Vec3i pos) {
return new MutableBox(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1);
}
public static MutableBox from(SectionPos pos) {
return new MutableBox(pos.minBlockX(), pos.minBlockY(), pos.minBlockZ(), pos.maxBlockX() + 1, pos.maxBlockY() + 1, pos.maxBlockZ() + 1);
}
public static MutableBox from(Vec3i start, Vec3i end) {
return new MutableBox(start.getX(), start.getY(), start.getZ(), end.getX() + 1, end.getY() + 1, end.getZ() + 1);
}
public static MutableBox ofRadius(int radius) {
return new MutableBox(-radius, -radius, -radius, radius + 1, radius + 1, radius + 1);
}
public static Box containingAll(Collection<BlockPos> positions) {
if (positions.isEmpty()) {
return new MutableBox();
}
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (BlockPos pos : positions) {
minX = Math.min(minX, pos.getX());
minY = Math.min(minY, pos.getY());
minZ = Math.min(minZ, pos.getZ());
maxX = Math.max(maxX, pos.getX());
maxY = Math.max(maxY, pos.getY());
maxZ = Math.max(maxZ, pos.getZ());
}
return new MutableBox(minX, minY, minZ, maxX, maxY, maxZ);
}
@Override
public int getMinX() {
return minX;
}
@Override
public int getMinY() {
return minY;
}
@Override
public int getMinZ() {
return minZ;
}
@Override
public int getMaxX() {
return maxX;
}
@Override
public int getMaxY() {
return maxY;
}
@Override
public int getMaxZ() {
return maxZ;
}
public void setMinX(int minX) {
this.minX = minX;
}
public void setMinY(int minY) {
this.minY = minY;
}
public MutableBox setMinZ(int minZ) {
this.minZ = minZ;
return this;
}
public void setMaxX(int maxX) {
this.maxX = maxX;
}
public void setMaxY(int maxY) {
this.maxY = maxY;
}
public void setMaxZ(int maxZ) {
this.maxZ = maxZ;
}
public void setMin(int x, int y, int z) {
minX = x;
minY = y;
minZ = z;
}
public void setMax(int x, int y, int z) {
maxX = x;
maxY = y;
maxZ = z;
}
public void setMin(Vec3i v) {
setMin(v.getX(), v.getY(), v.getZ());
}
public void setMax(Vec3i v) {
setMax(v.getX(), v.getY(), v.getZ());
}
public void assign(Box other) {
minX = other.getMinX();
minY = other.getMinY();
minZ = other.getMinZ();
maxX = other.getMaxX();
maxY = other.getMaxY();
maxZ = other.getMaxZ();
}
public void assign(AABB other) {
minX = (int) Math.floor(other.minX);
minY = (int) Math.floor(other.minY);
minZ = (int) Math.floor(other.minZ);
maxX = (int) Math.ceil(other.maxX);
maxY = (int) Math.ceil(other.maxY);
maxZ = (int) Math.ceil(other.maxZ);
}
public void assign(Vec3i start, Vec3i end) {
minX = start.getX();
minY = start.getY();
minZ = start.getZ();
maxX = end.getX() + 1;
maxY = end.getY() + 1;
maxZ = end.getZ() + 1;
}
public void unionAssign(Box other) {
minX = Math.min(this.minX, other.getMinX());
minY = Math.min(this.minY, other.getMinY());
minZ = Math.min(this.minZ, other.getMinZ());
maxX = Math.max(this.maxX, other.getMaxX());
maxY = Math.max(this.maxY, other.getMaxY());
maxZ = Math.max(this.maxZ, other.getMaxZ());
}
public void unionAssign(AABB other) {
minX = Math.min(this.minX, (int) Math.floor(other.minX));
minY = Math.min(this.minY, (int) Math.floor(other.minY));
minZ = Math.min(this.minZ, (int) Math.floor(other.minZ));
maxX = Math.max(this.maxX, (int) Math.ceil(other.maxX));
maxY = Math.max(this.maxY, (int) Math.ceil(other.maxY));
maxZ = Math.max(this.maxZ, (int) Math.ceil(other.maxZ));
}
public void intersectAssign(Box other) {
minX = Math.max(this.minX, other.getMinX());
minY = Math.max(this.minY, other.getMinY());
minZ = Math.max(this.minZ, other.getMinZ());
maxX = Math.min(this.maxX, other.getMaxX());
maxY = Math.min(this.maxY, other.getMaxY());
maxZ = Math.min(this.maxZ, other.getMaxZ());
}
public void fixMinMax() {
int minX = Math.min(this.minX, this.maxX);
int minY = Math.min(this.minY, this.maxY);
int minZ = Math.min(this.minZ, this.maxZ);
int maxX = Math.max(this.minX, this.maxX);
int maxY = Math.max(this.minY, this.maxY);
int maxZ = Math.max(this.minZ, this.maxZ);
this.minX = minX;
this.minY = minY;
this.minZ = minZ;
this.maxX = maxX;
this.maxY = maxY;
this.maxZ = maxZ;
}
public void translate(int x, int y, int z) {
minX = minX + x;
maxX = maxX + x;
minY = minY + y;
maxY = maxY + y;
minZ = minZ + z;
maxZ = maxZ + z;
}
public void translate(Vec3i by) {
translate(by.getX(), by.getY(), by.getZ());
}
public void grow(int x, int y, int z) {
minX = minX - x;
minY = minY - y;
minZ = minZ - z;
maxX = maxX + x;
maxY = maxY + y;
maxZ = maxZ + z;
}
public void grow(int s) {
this.grow(s, s, s);
}
/**
* Grow this box to have power of 2 side lengths, scaling from the minimum coords.
*/
public void nextPowerOf2() {
int sizeX = Mth.smallestEncompassingPowerOfTwo(sizeX());
int sizeY = Mth.smallestEncompassingPowerOfTwo(sizeY());
int sizeZ = Mth.smallestEncompassingPowerOfTwo(sizeZ());
maxX = minX + sizeX;
maxY = minY + sizeY;
maxZ = minZ + sizeZ;
}
/**
* Grow this box to have power of 2 side length, scaling from the center.
*/
public void nextPowerOf2Centered() {
int sizeX = sizeX();
int sizeY = sizeY();
int sizeZ = sizeZ();
int newSizeX = Mth.smallestEncompassingPowerOfTwo(sizeX);
int newSizeY = Mth.smallestEncompassingPowerOfTwo(sizeY);
int newSizeZ = Mth.smallestEncompassingPowerOfTwo(sizeZ);
int diffX = newSizeX - sizeX;
int diffY = newSizeY - sizeY;
int diffZ = newSizeZ - sizeZ;
minX = minX - diffX / 2; // floor division for the minimums
minY = minY - diffY / 2;
minZ = minZ - diffZ / 2;
maxX = maxX + (diffX + 1) / 2; // ceiling divison for the maximums
maxY = maxY + (diffY + 1) / 2;
maxZ = maxZ + (diffZ + 1) / 2;
}
public void mirrorAbout(Direction.Axis axis) {
Vec3i axisVec = Direction.get(Direction.AxisDirection.POSITIVE, axis)
.getNormal();
int flipX = axisVec.getX() - 1;
int flipY = axisVec.getY() - 1;
int flipZ = axisVec.getZ() - 1;
int maxX = this.maxX * flipX;
int maxY = this.maxY * flipY;
int maxZ = this.maxZ * flipZ;
this.maxX = this.minX * flipX;
this.maxY = this.minY * flipY;
this.maxZ = this.minZ * flipZ;
this.minX = maxX;
this.minY = maxY;
this.minZ = maxZ;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof Box that)) return false;
return this.sameAs(that);
}
@Override
public int hashCode() {
int result = minX;
result = 31 * result + minY;
result = 31 * result + minZ;
result = 31 * result + maxX;
result = 31 * result + maxY;
result = 31 * result + maxZ;
return result;
}
@Override
public String toString() {
return "(" + minX + ", " + minY + ", " + minZ + ")->(" + maxX + ", " + maxY + ", " + maxZ + ')';
}
}

View File

@ -1,26 +0,0 @@
package dev.engine_room.flywheel.lib.light;
/**
* Utility class for bit-twiddling light.
*/
public class LightPacking {
public static int getBlock(short packed) {
return (packed >> 4) & 0xF;
}
public static int getSky(short packed) {
return (packed >> 12) & 0xF;
}
public static byte packLightNibbles(byte block, byte sky) {
return (byte) (block | (sky << 4));
}
public static int getBlock(byte packed) {
return packed & 0xF;
}
public static int getSky(byte packed) {
return (packed >> 4) & 0xF;
}
}

View File

@ -1,217 +0,0 @@
package dev.engine_room.flywheel.lib.light;
import org.lwjgl.system.MemoryUtil;
import dev.engine_room.flywheel.lib.box.Box;
import dev.engine_room.flywheel.lib.box.MutableBox;
import dev.engine_room.flywheel.lib.memory.MemoryBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.LightLayer;
public class LightVolume implements Box {
protected final BlockAndTintGetter level;
protected final MutableBox box = new MutableBox();
protected MemoryBlock lightData;
public LightVolume(BlockAndTintGetter level, Box sampleVolume) {
this.level = level;
this.setBox(sampleVolume);
this.lightData = MemoryBlock.malloc(this.box.volume() * 2);
}
public Box getVolume() {
return box;
}
@Override
public int getMinX() {
return box.getMinX();
}
@Override
public int getMinY() {
return box.getMinY();
}
@Override
public int getMinZ() {
return box.getMinZ();
}
@Override
public int getMaxX() {
return box.getMaxX();
}
@Override
public int getMaxY() {
return box.getMaxY();
}
@Override
public int getMaxZ() {
return box.getMaxZ();
}
public boolean isInvalid() {
return lightData == null;
}
protected void setBox(Box box) {
this.box.assign(box);
}
public short getPackedLight(int x, int y, int z) {
if (box.contains(x, y, z)) {
return MemoryUtil.memGetShort(levelPosToPtr(x, y, z));
} else {
return 0;
}
}
public void move(Box newSampleVolume) {
if (lightData == null) return;
setBox(newSampleVolume);
int neededCapacity = box.volume() * 2;
if (neededCapacity > lightData.size()) {
lightData = lightData.realloc(neededCapacity);
}
initialize();
}
/**
* Completely (re)populate this volume with block and sky lighting data.
* This is expensive and should be avoided.
*/
public void initialize() {
if (lightData == null) return;
copyLight(getVolume());
markDirty();
}
protected void markDirty() {
// noop
}
public void delete() {
lightData.free();
lightData = null;
}
/**
* Copy all light from the level into this volume.
*
* @param levelVolume the region in the level to copy data from.
*/
public void copyLight(Box levelVolume) {
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
int xShift = box.getMinX();
int yShift = box.getMinY();
int zShift = box.getMinZ();
levelVolume.forEachContained((x, y, z) -> {
pos.set(x, y, z);
int block = this.level.getBrightness(LightLayer.BLOCK, pos);
int sky = this.level.getBrightness(LightLayer.SKY, pos);
writeLight(x - xShift, y - yShift, z - zShift, block, sky);
});
}
protected void writeLight(int x, int y, int z, int block, int sky) {
byte b = (byte) ((block & 0xF) << 4);
byte s = (byte) ((sky & 0xF) << 4);
long ptr = boxPosToPtr(x, y, z);
MemoryUtil.memPutByte(ptr, b);
MemoryUtil.memPutByte(ptr + 1, s);
}
/**
* Copy block light from the level into this volume.
*
* @param levelVolume the region in the level to copy data from.
*/
public void copyBlock(Box levelVolume) {
var pos = new BlockPos.MutableBlockPos();
int xShift = box.getMinX();
int yShift = box.getMinY();
int zShift = box.getMinZ();
levelVolume.forEachContained((x, y, z) -> {
int light = this.level.getBrightness(LightLayer.BLOCK, pos.set(x, y, z));
writeBlock(x - xShift, y - yShift, z - zShift, light);
});
}
protected void writeBlock(int x, int y, int z, int block) {
byte b = (byte) ((block & 0xF) << 4);
MemoryUtil.memPutByte(boxPosToPtr(x, y, z), b);
}
/**
* Copy sky light from the level into this volume.
*
* @param levelVolume the region in the level to copy data from.
*/
public void copySky(Box levelVolume) {
var pos = new BlockPos.MutableBlockPos();
int xShift = box.getMinX();
int yShift = box.getMinY();
int zShift = box.getMinZ();
levelVolume.forEachContained((x, y, z) -> {
int light = this.level.getBrightness(LightLayer.SKY, pos.set(x, y, z));
writeSky(x - xShift, y - yShift, z - zShift, light);
});
}
protected void writeSky(int x, int y, int z, int sky) {
byte s = (byte) ((sky & 0xF) << 4);
MemoryUtil.memPutByte(boxPosToPtr(x, y, z) + 1, s);
}
protected long levelPosToPtr(int x, int y, int z) {
return lightData.ptr() + levelPosToPtrOffset(x, y, z);
}
protected long boxPosToPtr(int x, int y, int z) {
return lightData.ptr() + boxPosToPtrOffset(x, y, z);
}
protected int levelPosToPtrOffset(int x, int y, int z) {
x -= box.getMinX();
y -= box.getMinY();
z -= box.getMinZ();
return boxPosToPtrOffset(x, y, z);
}
protected int boxPosToPtrOffset(int x, int y, int z) {
return (x + box.sizeX() * (y + z * box.sizeY())) * 2;
}
public void onLightUpdate(LightLayer type, SectionPos pos) {
if (lightData == null) return;
MutableBox vol = MutableBox.from(pos);
if (!vol.intersects(getVolume())) return;
vol.intersectAssign(getVolume()); // compute the region contained by us that has dirty lighting data.
if (type == LightLayer.BLOCK) copyBlock(vol);
else if (type == LightLayer.SKY) copySky(vol);
markDirty();
}
}

View File

@ -1,18 +1,18 @@
package dev.engine_room.flywheel.lib.visual;
import java.util.function.LongConsumer;
import org.jetbrains.annotations.Nullable;
import org.joml.FrustumIntersection;
import dev.engine_room.flywheel.api.visual.BlockEntityVisual;
import dev.engine_room.flywheel.api.visual.DynamicVisual;
import dev.engine_room.flywheel.api.visual.LitVisual;
import dev.engine_room.flywheel.api.visual.LightUpdatedVisual;
import dev.engine_room.flywheel.api.visual.ShaderLightVisual;
import dev.engine_room.flywheel.api.visual.TickableVisual;
import dev.engine_room.flywheel.api.visualization.VisualManager;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
import dev.engine_room.flywheel.lib.instance.FlatLit;
import dev.engine_room.flywheel.lib.math.MoreMath;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
@ -26,6 +26,8 @@ import net.minecraft.world.level.block.state.BlockState;
* <ul>
* <li>{@link DynamicVisual}</li>
* <li>{@link TickableVisual}</li>
* <li>{@link LightUpdatedVisual}</li>
* <li>{@link ShaderLightVisual}</li>
* </ul>
* See the interfaces' documentation for more information about each one.
*
@ -34,13 +36,13 @@ import net.minecraft.world.level.block.state.BlockState;
*
* @param <T> The type of {@link BlockEntity}.
*/
public abstract class AbstractBlockEntityVisual<T extends BlockEntity> extends AbstractVisual implements BlockEntityVisual<T>, LitVisual {
public abstract class AbstractBlockEntityVisual<T extends BlockEntity> extends AbstractVisual implements BlockEntityVisual<T>, LightUpdatedVisual {
protected final T blockEntity;
protected final BlockPos pos;
protected final BlockPos visualPos;
protected final BlockState blockState;
@Nullable
protected LitVisual.Notifier notifier;
protected SectionCollector lightSections;
public AbstractBlockEntityVisual(VisualizationContext ctx, T blockEntity, float partialTick) {
super(ctx, blockEntity.getLevel(), partialTick);
@ -51,13 +53,9 @@ public abstract class AbstractBlockEntityVisual<T extends BlockEntity> extends A
}
@Override
public void setLightSectionNotifier(Notifier notifier) {
this.notifier = notifier;
}
@Override
public void collectLightSections(LongConsumer consumer) {
consumer.accept(SectionPos.asLong(pos));
public void setSectionCollector(SectionCollector sectionCollector) {
this.lightSections = sectionCollector;
lightSections.sections(LongSet.of(SectionPos.asLong(pos)));
}
/**

View File

@ -1,6 +1,5 @@
package dev.engine_room.flywheel.lib.visual;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
@ -24,8 +23,8 @@ public class SimpleBlockEntityVisualizer<T extends BlockEntity> implements Block
}
@Override
public List<BlockEntityVisual<? super T>> createVisual(VisualizationContext ctx, T blockEntity, float partialTick) {
return List.of(visualFactory.create(ctx, blockEntity, partialTick));
public BlockEntityVisual<? super T> createVisual(VisualizationContext ctx, T blockEntity, float partialTick) {
return visualFactory.create(ctx, blockEntity, partialTick);
}
@Override

View File

@ -1,6 +1,5 @@
package dev.engine_room.flywheel.lib.visual;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
@ -24,8 +23,8 @@ public class SimpleEntityVisualizer<T extends Entity> implements EntityVisualize
}
@Override
public List<EntityVisual<? super T>> createVisual(VisualizationContext ctx, T entity, float partialTick) {
return List.of(visualFactory.create(ctx, entity, partialTick));
public EntityVisual<? super T> createVisual(VisualizationContext ctx, T entity, float partialTick) {
return visualFactory.create(ctx, entity, partialTick);
}
@Override

View File

@ -1,2 +1,8 @@
void flw_materialFragment() {
#ifdef FLW_EMBEDDED
vec2 embeddedLight;
if (flw_light(flw_vertexPos.xyz, flw_vertexNormal, embeddedLight)) {
flw_fragLight = max(flw_fragLight, embeddedLight);
}
#endif
}

View File

@ -24,7 +24,7 @@ abstract class ClientChunkCacheMixin {
var manager = VisualizationManagerImpl.get(level);
if (manager != null) {
manager.enqueueLightUpdateSection(pos.asLong());
manager.onLightUpdate(pos.asLong());
}
}
}

View File

@ -17,7 +17,6 @@ import dev.engine_room.flywheel.api.event.RenderStage;
import dev.engine_room.flywheel.api.instance.Instance;
import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.api.task.TaskExecutor;
import dev.engine_room.flywheel.api.visual.BlockEntityVisual;
import dev.engine_room.flywheel.api.visual.DynamicVisual;
import dev.engine_room.flywheel.api.visual.Effect;
import dev.engine_room.flywheel.api.visual.TickableVisual;
@ -46,6 +45,7 @@ import dev.engine_room.flywheel.lib.task.RaisePlan;
import dev.engine_room.flywheel.lib.task.SimplePlan;
import dev.engine_room.flywheel.lib.util.LevelAttached;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.client.Minecraft;
import net.minecraft.core.Vec3i;
import net.minecraft.server.level.BlockDestructionProgress;
@ -105,6 +105,15 @@ public class VisualizationManagerImpl implements VisualizationManager {
.ifTrue(recreate)
.ifFalse(update)
.plan()
.then(SimplePlan.of(() -> {
if (blockEntities.areGpuLightSectionsDirty() || entities.areGpuLightSectionsDirty() || effects.areGpuLightSectionsDirty()) {
var out = new LongOpenHashSet();
out.addAll(blockEntities.gpuLightSections());
out.addAll(entities.gpuLightSections());
out.addAll(effects.gpuLightSections());
engine.lightSections(out);
}
}))
.then(RaisePlan.raise(frameVisualsFlag))
.then(engine.createFramePlan())
.then(RaisePlan.raise(frameFlag));
@ -262,23 +271,21 @@ public class VisualizationManagerImpl implements VisualizationManager {
continue;
}
var visualList = blockEntities.getStorage()
var visual = blockEntities.getStorage()
.visualAtPos(entry.getLongKey());
if (visualList == null || visualList.isEmpty()) {
if (visual == null) {
// The block doesn't have a visual, this is probably the common case.
continue;
}
List<Instance> instances = new ArrayList<>();
for (BlockEntityVisual<?> visual : visualList) {
visual.collectCrumblingInstances(instance -> {
if (instance != null) {
instances.add(instance);
}
});
}
visual.collectCrumblingInstances(instance -> {
if (instance != null) {
instances.add(instance);
}
});
if (instances.isEmpty()) {
// The visual doesn't want to render anything crumbling.
@ -295,6 +302,12 @@ public class VisualizationManagerImpl implements VisualizationManager {
}
}
public void onLightUpdate(long section) {
blockEntities.onLightUpdate(section);
entities.onLightUpdate(section);
effects.onLightUpdate(section);
}
/**
* Free all acquired resources and delete this manager.
*/
@ -308,13 +321,4 @@ public class VisualizationManagerImpl implements VisualizationManager {
effects.invalidate();
engine.delete();
}
public void enqueueLightUpdateSection(long section) {
blockEntities.getStorage()
.enqueueLightUpdateSection(section);
entities.getStorage()
.enqueueLightUpdateSection(section);
effects.getStorage()
.enqueueLightUpdateSection(section);
}
}

View File

@ -1,6 +1,5 @@
package dev.engine_room.flywheel.impl.visualization.manager;
import java.util.List;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
@ -17,14 +16,14 @@ import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
public class BlockEntityStorage extends Storage<BlockEntity> {
private final Long2ObjectMap<List<? extends BlockEntityVisual<?>>> posLookup = new Long2ObjectOpenHashMap<>();
private final Long2ObjectMap<BlockEntityVisual<?>> posLookup = new Long2ObjectOpenHashMap<>();
public BlockEntityStorage(Supplier<VisualizationContext> visualizationContextSupplier) {
super(visualizationContextSupplier);
}
@Nullable
public List<? extends BlockEntityVisual<?>> visualAtPos(long pos) {
public BlockEntityVisual<?> visualAtPos(long pos) {
return posLookup.get(pos);
}
@ -53,24 +52,37 @@ public class BlockEntityStorage extends Storage<BlockEntity> {
}
@Override
protected List<? extends BlockEntityVisual<?>> createRaw(BlockEntity obj, float partialTick) {
@Nullable
protected BlockEntityVisual<?> createRaw(BlockEntity obj, float partialTick) {
var visualizer = VisualizationHelper.getVisualizer(obj);
if (visualizer == null) {
return List.of();
return null;
}
var visualList = visualizer.createVisual(visualizationContextSupplier.get(), obj, partialTick);
var visual = visualizer.createVisual(visualizationContextSupplier.get(), obj, partialTick);
BlockPos blockPos = obj.getBlockPos();
posLookup.put(blockPos.asLong(), visualList);
posLookup.put(blockPos.asLong(), visual);
return visualList;
return visual;
}
@Override
public void remove(BlockEntity obj) {
super.remove(obj);
posLookup.remove(obj.getBlockPos()
.asLong());
super.remove(obj);
}
@Override
public void recreateAll(float partialTick) {
posLookup.clear();
super.recreateAll(partialTick);
}
@Override
public void invalidate() {
posLookup.clear();
super.invalidate();
}
}

View File

@ -1,6 +1,5 @@
package dev.engine_room.flywheel.impl.visualization.manager;
import java.util.List;
import java.util.function.Supplier;
import dev.engine_room.flywheel.api.visual.Effect;
@ -14,7 +13,7 @@ public class EffectStorage extends Storage<Effect> {
}
@Override
protected List<? extends EffectVisual<?>> createRaw(Effect obj, float partialTick) {
protected EffectVisual<?> createRaw(Effect obj, float partialTick) {
return obj.visualize(visualizationContextSupplier.get(), partialTick);
}

View File

@ -1,6 +1,5 @@
package dev.engine_room.flywheel.impl.visualization.manager;
import java.util.List;
import java.util.function.Supplier;
import dev.engine_room.flywheel.api.visual.EntityVisual;
@ -16,10 +15,10 @@ public class EntityStorage extends Storage<Entity> {
}
@Override
protected List<? extends EntityVisual<?>> createRaw(Entity obj, float partialTick) {
protected EntityVisual<?> createRaw(Entity obj, float partialTick) {
var visualizer = VisualizationHelper.getVisualizer(obj);
if (visualizer == null) {
return List.of();
return null;
}
return visualizer.createVisual(visualizationContextSupplier.get(), obj, partialTick);

View File

@ -10,6 +10,7 @@ import dev.engine_room.flywheel.api.visualization.VisualManager;
import dev.engine_room.flywheel.impl.visualization.storage.Storage;
import dev.engine_room.flywheel.impl.visualization.storage.Transaction;
import dev.engine_room.flywheel.lib.task.SimplePlan;
import it.unimi.dsi.fastutil.longs.LongSet;
public class VisualManagerImpl<T, S extends Storage<T>> implements VisualManager<T> {
private final Queue<Transaction<T>> queue = new ConcurrentLinkedQueue<>();
@ -26,7 +27,8 @@ public class VisualManagerImpl<T, S extends Storage<T>> implements VisualManager
@Override
public int getVisualCount() {
return getStorage().visualCount();
return getStorage().getAllVisuals()
.size();
}
@Override
@ -52,10 +54,6 @@ public class VisualManagerImpl<T, S extends Storage<T>> implements VisualManager
queue.add(Transaction.update(obj));
}
public void invalidate() {
getStorage().invalidate();
}
public void processQueue(float partialTick) {
var storage = getStorage();
Transaction<T> transaction;
@ -73,4 +71,23 @@ public class VisualManagerImpl<T, S extends Storage<T>> implements VisualManager
return SimplePlan.<TickableVisual.Context>of(context -> processQueue(1))
.then(storage.tickPlan());
}
public void onLightUpdate(long section) {
getStorage().lightUpdatedVisuals()
.onLightUpdate(section);
}
public boolean areGpuLightSectionsDirty() {
return getStorage().shaderLightVisuals()
.isDirty();
}
public LongSet gpuLightSections() {
return getStorage().shaderLightVisuals()
.sections();
}
public void invalidate() {
getStorage().invalidate();
}
}

View File

@ -10,13 +10,12 @@ import java.util.concurrent.atomic.AtomicLong;
import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.api.task.TaskExecutor;
import dev.engine_room.flywheel.api.visual.DynamicVisual;
import dev.engine_room.flywheel.api.visual.LitVisual;
import dev.engine_room.flywheel.api.visual.LightUpdatedVisual;
import dev.engine_room.flywheel.lib.task.Distribute;
import dev.engine_room.flywheel.lib.task.SimplyComposedPlan;
import dev.engine_room.flywheel.lib.task.Synchronizer;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArraySet;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
@ -24,15 +23,15 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList;
/**
* Keeps track of what chunks/sections each listener is in, so we can update exactly what needs to be updated.
*/
public class LitVisualStorage {
public class LightUpdatedVisualStorage {
private static final long NEVER_UPDATED = Long.MIN_VALUE;
private static final long INITIAL_UPDATE_ID = NEVER_UPDATED + 1;
private final Map<LitVisual, LongSet> visuals2Sections = new WeakHashMap<>();
private final Long2ObjectMap<List<Updater>> sections2Visuals = new Long2ObjectOpenHashMap<>();
private final Map<LightUpdatedVisual, LongSet> visual2Sections = new WeakHashMap<>();
private final Long2ObjectMap<List<Updater>> section2Updaters = new Long2ObjectOpenHashMap<>();
private final Queue<LitVisual> movedVisuals = new ConcurrentLinkedQueue<>();
private final LongSet sectionsUpdatedThisFrame = new LongOpenHashSet();
private final Queue<MovedVisual> movedVisuals = new ConcurrentLinkedQueue<>();
private long updateId = INITIAL_UPDATE_ID;
@ -54,9 +53,9 @@ public class LitVisualStorage {
Updater.Context updaterContext = new Updater.Context(updateId, context.partialTick());
for (long section : sectionsUpdatedThisFrame) {
var visuals = sections2Visuals.get(section);
if (visuals != null && !visuals.isEmpty()) {
taskExecutor.execute(() -> Distribute.tasks(taskExecutor, updaterContext, sync, visuals, Updater::updateLight));
var updaters = section2Updaters.get(section);
if (updaters != null && !updaters.isEmpty()) {
taskExecutor.execute(() -> Distribute.tasks(taskExecutor, updaterContext, sync, updaters, Updater::updateLight));
} else {
sync.decrementAndEventuallyRun();
}
@ -65,11 +64,11 @@ public class LitVisualStorage {
}
private void processMoved() {
LitVisual visual;
while ((visual = movedVisuals.poll()) != null) {
MovedVisual moved;
while ((moved = movedVisuals.poll()) != null) {
// If the visual isn't there when we try to remove it that means it was deleted before we got to it.
if (remove(visual)) {
add(visual);
if (remove(moved.visual)) {
addInner(moved.visual, moved.tracker);
}
}
}
@ -86,72 +85,69 @@ public class LitVisualStorage {
return out;
}
public boolean isEmpty() {
return visuals2Sections.isEmpty();
public void add(LightUpdatedVisual visual, SectionTracker tracker) {
var moved = new MovedVisual(visual, tracker);
tracker.addListener(() -> movedVisuals.add(moved));
addInner(visual, tracker);
}
public void setNotifierAndAdd(LitVisual visual) {
visual.setLightSectionNotifier(new LitVisualNotifierImpl(visual));
add(visual);
}
private void addInner(LightUpdatedVisual visual, SectionTracker tracker) {
if (tracker.sections().isEmpty()) {
// Add the visual to the map even if sections is empty, this way we can distinguish from deleted visuals
visual2Sections.put(visual, LongSet.of());
private void add(LitVisual visual) {
LongSet sections = new LongArraySet();
visual.collectLightSections(sections::add);
// Add the visual to the map even if sections is empty, this way we can distinguish from deleted visuals
visuals2Sections.put(visual, sections);
// Don't bother creating an updater if the visual isn't in any sections.
if (sections.isEmpty()) {
// Don't bother creating an updater if the visual isn't in any sections.
return;
}
var sections = tracker.sections();
visual2Sections.put(visual, sections);
var updater = createUpdater(visual, sections.size());
for (long section : sections) {
sections2Visuals.computeIfAbsent(section, $ -> new ObjectArrayList<>())
section2Updaters.computeIfAbsent(section, $ -> new ObjectArrayList<>())
.add(updater);
}
}
public void enqueueLightUpdateSection(long section) {
sectionsUpdatedThisFrame.add(section);
}
/**
* Remove the visual from this storage.
*
* @param visual The visual to remove.
* @return {@code true} if the visual was removed, {@code false} otherwise.
*/
public boolean remove(LitVisual visual) {
var sections = visuals2Sections.remove(visual);
public boolean remove(LightUpdatedVisual visual) {
var sections = visual2Sections.remove(visual);
if (sections == null) {
return false;
}
for (long section : sections) {
List<Updater> listeners = sections2Visuals.get(section);
if (listeners != null) {
listeners.remove(indexOfUpdater(listeners, visual));
List<Updater> updaters = section2Updaters.get(section);
if (updaters != null) {
updaters.remove(indexOfUpdater(updaters, visual));
}
}
return true;
}
public void clear() {
visuals2Sections.clear();
sections2Visuals.clear();
sectionsUpdatedThisFrame.clear();
public void onLightUpdate(long section) {
sectionsUpdatedThisFrame.add(section);
}
private static int indexOfUpdater(List<Updater> listeners, LitVisual visual) {
for (int i = 0; i < listeners.size(); i++) {
if (listeners.get(i)
public void clear() {
visual2Sections.clear();
section2Updaters.clear();
sectionsUpdatedThisFrame.clear();
movedVisuals.clear();
}
private static int indexOfUpdater(List<Updater> updaters, LightUpdatedVisual visual) {
for (int i = 0; i < updaters.size(); i++) {
if (updaters.get(i)
.visual() == visual) {
return i;
}
@ -159,7 +155,7 @@ public class LitVisualStorage {
return -1;
}
private static Updater createUpdater(LitVisual visual, int sectionCount) {
private static Updater createUpdater(LightUpdatedVisual visual, int sectionCount) {
if (sectionCount == 1) {
return new Updater.Simple(visual);
} else {
@ -168,13 +164,13 @@ public class LitVisualStorage {
}
// Breaking this into 2 separate cases allows us to avoid the overhead of atomics in the common case.
sealed interface Updater {
private sealed interface Updater {
void updateLight(Context ctx);
LitVisual visual();
LightUpdatedVisual visual();
// The visual is only in one section. In this case, we can just update the visual directly.
record Simple(LitVisual visual) implements Updater {
record Simple(LightUpdatedVisual visual) implements Updater {
@Override
public void updateLight(Context ctx) {
visual.updateLight(ctx.partialTick);
@ -183,7 +179,7 @@ public class LitVisualStorage {
// The visual is in multiple sections. Here we need to make sure that the visual only gets updated once,
// even when multiple sections it was contained in are updated at the same time.
record Synced(LitVisual visual, AtomicLong updateId) implements Updater {
record Synced(LightUpdatedVisual visual, AtomicLong updateId) implements Updater {
@Override
public void updateLight(Context ctx) {
// Different update ID means we won, so we can update the visual.
@ -198,16 +194,6 @@ public class LitVisualStorage {
}
}
private final class LitVisualNotifierImpl implements LitVisual.Notifier {
private final LitVisual litVisual;
private LitVisualNotifierImpl(LitVisual litVisual) {
this.litVisual = litVisual;
}
@Override
public void notifySectionsChanged() {
movedVisuals.add(litVisual);
}
}
private record MovedVisual(LightUpdatedVisual visual, SectionTracker tracker) {
}
}

View File

@ -0,0 +1,33 @@
package dev.engine_room.flywheel.impl.visualization.storage;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Unmodifiable;
import dev.engine_room.flywheel.api.visual.SectionTrackedVisual;
import it.unimi.dsi.fastutil.longs.LongArraySet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.longs.LongSets;
public class SectionTracker implements SectionTrackedVisual.SectionCollector {
private final List<Runnable> listeners = new ArrayList<>(2);
@Unmodifiable
private LongSet sections = LongSet.of();
@Unmodifiable
public LongSet sections() {
return sections;
}
@Override
public void sections(LongSet sections) {
this.sections = LongSets.unmodifiable(new LongArraySet(sections));
listeners.forEach(Runnable::run);
}
public void addListener(Runnable listener) {
listeners.add(listener);
}
}

View File

@ -0,0 +1,53 @@
package dev.engine_room.flywheel.impl.visualization.storage;
import java.util.Map;
import dev.engine_room.flywheel.api.visual.ShaderLightVisual;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
public class ShaderLightVisualStorage {
private final Map<ShaderLightVisual, SectionTracker> trackers = new Reference2ReferenceOpenHashMap<>();
private final LongSet sections = new LongOpenHashSet();
private boolean isDirty;
public LongSet sections() {
if (isDirty) {
sections.clear();
for (var tracker : trackers.values()) {
sections.addAll(tracker.sections());
}
isDirty = false;
}
return sections;
}
public boolean isDirty() {
return isDirty;
}
public void markDirty() {
isDirty = true;
}
public void add(ShaderLightVisual visual, SectionTracker tracker) {
trackers.put(visual, tracker);
tracker.addListener(this::markDirty);
if (!tracker.sections().isEmpty()) {
markDirty();
}
}
public void remove(ShaderLightVisual visual) {
trackers.remove(visual);
}
public void clear() {
trackers.clear();
markDirty();
}
}

View File

@ -1,13 +1,18 @@
package dev.engine_room.flywheel.impl.visualization.storage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.api.visual.DynamicVisual;
import dev.engine_room.flywheel.api.visual.LitVisual;
import dev.engine_room.flywheel.api.visual.LightUpdatedVisual;
import dev.engine_room.flywheel.api.visual.SectionTrackedVisual;
import dev.engine_room.flywheel.api.visual.ShaderLightVisual;
import dev.engine_room.flywheel.api.visual.TickableVisual;
import dev.engine_room.flywheel.api.visual.Visual;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
@ -20,161 +25,37 @@ import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
public abstract class Storage<T> {
protected final Supplier<VisualizationContext> visualizationContextSupplier;
private final Map<T, Visual> visuals = new Reference2ObjectOpenHashMap<>();
protected final PlanMap<DynamicVisual, DynamicVisual.Context> dynamicVisuals = new PlanMap<>();
protected final PlanMap<TickableVisual, TickableVisual.Context> tickableVisuals = new PlanMap<>();
protected final List<SimpleDynamicVisual> simpleDynamicVisuals = new ArrayList<>();
protected final List<SimpleTickableVisual> simpleTickableVisuals = new ArrayList<>();
protected final LitVisualStorage litVisuals = new LitVisualStorage();
private final Map<T, List<? extends Visual>> visuals = new Reference2ObjectOpenHashMap<>();
protected final LightUpdatedVisualStorage lightUpdatedVisuals = new LightUpdatedVisualStorage();
protected final ShaderLightVisualStorage shaderLightVisuals = new ShaderLightVisualStorage();
public Storage(Supplier<VisualizationContext> visualizationContextSupplier) {
this.visualizationContextSupplier = visualizationContextSupplier;
}
public int visualCount() {
int out = 0;
for (var visualList : visuals.values()) {
out += visualList.size();
}
return out;
public Collection<Visual> getAllVisuals() {
return visuals.values();
}
public void add(T obj, float partialTick) {
var visualList = this.visuals.get(obj);
if (visualList == null) {
create(obj, partialTick);
}
}
public void remove(T obj) {
var visualList = this.visuals.remove(obj);
if (visualList == null || visualList.isEmpty()) {
return;
}
for (Visual visual : visualList) {
remove(visual);
}
}
public void update(T obj, float partialTick) {
var visualList = visuals.get(obj);
if (visualList == null || visualList.isEmpty()) {
return;
}
for (Visual visual : visualList) {
visual.update(partialTick);
}
}
public void recreateAll(float partialTick) {
tickableVisuals.clear();
dynamicVisuals.clear();
simpleTickableVisuals.clear();
simpleDynamicVisuals.clear();
litVisuals.clear();
visuals.replaceAll((obj, visuals) -> {
visuals.forEach(Visual::delete);
var out = createRaw(obj, partialTick);
if (out.isEmpty()) {
return null;
}
for (Visual visual : out) {
setup(visual);
}
return out;
});
}
public void invalidate() {
tickableVisuals.clear();
dynamicVisuals.clear();
litVisuals.clear();
for (var visualList : visuals.values()) {
for (Visual visual : visualList) {
visual.delete();
}
}
visuals.clear();
}
private void create(T obj, float partialTick) {
var visuals = createRaw(obj, partialTick);
if (visuals.isEmpty()) {
return;
}
this.visuals.put(obj, visuals);
for (Visual visual : visuals) {
setup(visual);
}
}
protected abstract List<? extends Visual> createRaw(T obj, float partialTick);
public Plan<DynamicVisual.Context> framePlan() {
return NestedPlan.of(dynamicVisuals, litVisuals.plan(), ForEachPlan.of(() -> simpleDynamicVisuals, SimpleDynamicVisual::beginFrame));
return NestedPlan.of(dynamicVisuals, lightUpdatedVisuals.plan(), ForEachPlan.of(() -> simpleDynamicVisuals, SimpleDynamicVisual::beginFrame));
}
public Plan<TickableVisual.Context> tickPlan() {
return NestedPlan.of(tickableVisuals, ForEachPlan.of(() -> simpleTickableVisuals, SimpleTickableVisual::tick));
}
public void enqueueLightUpdateSection(long section) {
litVisuals.enqueueLightUpdateSection(section);
public LightUpdatedVisualStorage lightUpdatedVisuals() {
return lightUpdatedVisuals;
}
private void setup(Visual visual) {
if (visual instanceof TickableVisual tickable) {
if (visual instanceof SimpleTickableVisual simpleTickable) {
simpleTickableVisuals.add(simpleTickable);
} else {
tickableVisuals.add(tickable, tickable.planTick());
}
}
if (visual instanceof DynamicVisual dynamic) {
if (visual instanceof SimpleDynamicVisual simpleDynamic) {
simpleDynamicVisuals.add(simpleDynamic);
} else {
dynamicVisuals.add(dynamic, dynamic.planFrame());
}
}
if (visual instanceof LitVisual lit) {
litVisuals.setNotifierAndAdd(lit);
}
}
private void remove(Visual visual) {
if (visual instanceof TickableVisual tickable) {
if (visual instanceof SimpleTickableVisual simpleTickable) {
simpleTickableVisuals.remove(simpleTickable);
} else {
tickableVisuals.remove(tickable);
}
}
if (visual instanceof DynamicVisual dynamic) {
if (visual instanceof SimpleDynamicVisual simpleDynamic) {
simpleDynamicVisuals.remove(simpleDynamic);
} else {
dynamicVisuals.remove(dynamic);
}
}
if (visual instanceof LitVisual lit) {
litVisuals.remove(lit);
}
visual.delete();
public ShaderLightVisualStorage shaderLightVisuals() {
return shaderLightVisuals;
}
/**
@ -183,4 +64,132 @@ public abstract class Storage<T> {
* @return true if the object is currently capable of being visualized.
*/
public abstract boolean willAccept(T obj);
public void add(T obj, float partialTick) {
Visual visual = visuals.get(obj);
if (visual == null) {
create(obj, partialTick);
}
}
public void remove(T obj) {
Visual visual = visuals.remove(obj);
if (visual == null) {
return;
}
if (visual instanceof DynamicVisual dynamic) {
if (visual instanceof SimpleDynamicVisual simpleDynamic) {
simpleDynamicVisuals.remove(simpleDynamic);
} else {
dynamicVisuals.remove(dynamic);
}
}
if (visual instanceof TickableVisual tickable) {
if (visual instanceof SimpleTickableVisual simpleTickable) {
simpleTickableVisuals.remove(simpleTickable);
} else {
tickableVisuals.remove(tickable);
}
}
if (visual instanceof LightUpdatedVisual lightUpdated) {
lightUpdatedVisuals.remove(lightUpdated);
}
if (visual instanceof ShaderLightVisual shaderLight) {
shaderLightVisuals.remove(shaderLight);
}
visual.delete();
}
public void update(T obj, float partialTick) {
Visual visual = visuals.get(obj);
if (visual == null) {
return;
}
visual.update(partialTick);
}
public void recreateAll(float partialTick) {
dynamicVisuals.clear();
tickableVisuals.clear();
simpleDynamicVisuals.clear();
simpleTickableVisuals.clear();
lightUpdatedVisuals.clear();
shaderLightVisuals.clear();
visuals.replaceAll((obj, visual) -> {
visual.delete();
var out = createRaw(obj, partialTick);
if (out != null) {
setup(out, partialTick);
}
return out;
});
}
private void create(T obj, float partialTick) {
var visual = createRaw(obj, partialTick);
if (visual != null) {
setup(visual, partialTick);
visuals.put(obj, visual);
}
}
@Nullable
protected abstract Visual createRaw(T obj, float partialTick);
private void setup(Visual visual, float partialTick) {
if (visual instanceof DynamicVisual dynamic) {
if (visual instanceof SimpleDynamicVisual simpleDynamic) {
simpleDynamicVisuals.add(simpleDynamic);
} else {
dynamicVisuals.add(dynamic, dynamic.planFrame());
}
}
if (visual instanceof TickableVisual tickable) {
if (visual instanceof SimpleTickableVisual simpleTickable) {
simpleTickableVisuals.add(simpleTickable);
} else {
tickableVisuals.add(tickable, tickable.planTick());
}
}
if (visual instanceof SectionTrackedVisual tracked) {
SectionTracker tracker = new SectionTracker();
// Give the visual a chance to invoke the collector.
tracked.setSectionCollector(tracker);
if (visual instanceof LightUpdatedVisual lightUpdated) {
lightUpdatedVisuals.add(lightUpdated, tracker);
lightUpdated.updateLight(partialTick);
}
if (visual instanceof ShaderLightVisual shaderLight) {
shaderLightVisuals.add(shaderLight, tracker);
}
}
}
public void invalidate() {
dynamicVisuals.clear();
tickableVisuals.clear();
simpleDynamicVisuals.clear();
simpleTickableVisuals.clear();
lightUpdatedVisuals.clear();
shaderLightVisuals.clear();
visuals.values()
.forEach(Visual::delete);
visuals.clear();
}
}

View File

@ -38,7 +38,6 @@ public class BellVisual extends AbstractBlockEntityVisual<BellBlockEntity> imple
bell.setChanged();
updateRotation(partialTick);
updateLight(partialTick);
}
private OrientedInstance createBellInstance() {

View File

@ -88,7 +88,6 @@ public class ChestVisual<T extends BlockEntity & LidBlockEntity> extends Abstrac
bottom.setChanged();
applyLidTransform(lidProgress.get(partialTick));
updateLight(partialTick);
}
private OrientedInstance createBottomInstance(Material texture) {

View File

@ -194,7 +194,7 @@ public class MinecartVisual<T extends AbstractMinecart> extends SimpleEntityVisu
body.setTransform(stack)
.setChanged();
// TODO: Use LitVisual if possible.
// TODO: Use LightUpdatedVisual/ShaderLightVisual if possible.
updateLight(partialTick);
}

View File

@ -67,8 +67,6 @@ public class ShulkerBoxVisual extends AbstractBlockEntityVisual<ShulkerBoxBlockE
base.setChanged();
lid = createLidInstance(texture).setTransform(stack);
lid.setChanged();
updateLight(partialTick);
}
private TransformedInstance createBaseInstance(Material texture) {

View File

@ -0,0 +1,7 @@
/// Get the light at the given world position.
/// This may be interpolated for smooth lighting.
bool flw_light(vec3 worldPos, out vec2 light);
/// Fetches the light value at the given block position.
/// Returns false if the light for the given block is not available.
bool flw_lightFetch(ivec3 blockPos, out vec2 light);

View File

@ -1,4 +1,5 @@
#include "flywheel:api/material.glsl"
#include "flywheel:api/common.glsl"
/*const*/ vec4 flw_vertexPos;
/*const*/ vec4 flw_vertexColor;
@ -24,10 +25,6 @@ vec4 flw_fogFilter(vec4 color);
// To be implemented by discard shaders.
bool flw_discardPredicate(vec4 finalColor);
// To be implemented by the context shader.
void flw_beginFragment();
void flw_endFragment();
sampler2D flw_diffuseTex;
sampler2D flw_overlayTex;
sampler2D flw_lightTex;

View File

@ -1,4 +1,5 @@
#include "flywheel:api/material.glsl"
#include "flywheel:api/common.glsl"
vec4 flw_vertexPos;
vec4 flw_vertexColor;
@ -17,7 +18,3 @@ void flw_transformBoundingSphere(in FlwInstance i, inout vec3 center, inout floa
// To be implemented by the material vertex shader.
void flw_materialVertex();
// To be implemented by the context shader.
void flw_beginVertex();
void flw_endVertex();