diff --git a/common/src/api/java/dev/engine_room/flywheel/api/backend/Engine.java b/common/src/api/java/dev/engine_room/flywheel/api/backend/Engine.java index ab7d41ee9..b6b7baa25 100644 --- a/common/src/api/java/dev/engine_room/flywheel/api/backend/Engine.java +++ b/common/src/api/java/dev/engine_room/flywheel/api/backend/Engine.java @@ -9,6 +9,7 @@ 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. + * + *

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. *
diff --git a/common/src/api/java/dev/engine_room/flywheel/api/visual/LightUpdatedVisual.java b/common/src/api/java/dev/engine_room/flywheel/api/visual/LightUpdatedVisual.java new file mode 100644 index 000000000..52015c436 --- /dev/null +++ b/common/src/api/java/dev/engine_room/flywheel/api/visual/LightUpdatedVisual.java @@ -0,0 +1,23 @@ +package dev.engine_room.flywheel.api.visual; + +/** + * A visual that listens to light updates. + * + *

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}.

+ */ +public non-sealed interface LightUpdatedVisual extends SectionTrackedVisual { + /** + * Called when a section this visual is contained in receives a light update. It is not called + * unconditionally after visual creation or {@link SectionCollector#sections}. + * + *

Even if multiple sections are updated at the same time, this method will only be called once.

+ * + *

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}.

+ */ + void updateLight(float partialTick); +} diff --git a/common/src/api/java/dev/engine_room/flywheel/api/visual/LitVisual.java b/common/src/api/java/dev/engine_room/flywheel/api/visual/LitVisual.java deleted file mode 100644 index 4f05e7687..000000000 --- a/common/src/api/java/dev/engine_room/flywheel/api/visual/LitVisual.java +++ /dev/null @@ -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. - * - *

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}.

- */ -public interface LitVisual extends Visual { - /** - * Set the notifier object. - * - *

This method is only called once right after the visual - * is created and before {@link #collectLightSections}.

- * - * @param notifier The notifier. - */ - void setLightSectionNotifier(Notifier notifier); - - /** - * Collect the sections that this visual is contained in. - * - *

This method is called upon visual creation, and the frame after - * {@link Notifier#notifySectionsChanged} is called.

- * - * @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. - * - *

Even if multiple sections are updated at the same time, this method will only be called once.

- * - *

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}.

- * - *

This method not is invoked automatically after visual creation.

- */ - 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. - *
- * The next frame, the impl will call {@link LitVisual#collectLightSections} again. - */ - void notifySectionsChanged(); - } -} diff --git a/common/src/api/java/dev/engine_room/flywheel/api/visual/SectionTrackedVisual.java b/common/src/api/java/dev/engine_room/flywheel/api/visual/SectionTrackedVisual.java new file mode 100644 index 000000000..a2189357a --- /dev/null +++ b/common/src/api/java/dev/engine_room/flywheel/api/visual/SectionTrackedVisual.java @@ -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. + * + *

This method is only called once, upon visual creation. + *
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); + } +} diff --git a/common/src/api/java/dev/engine_room/flywheel/api/visual/ShaderLightVisual.java b/common/src/api/java/dev/engine_room/flywheel/api/visual/ShaderLightVisual.java new file mode 100644 index 000000000..dcd4ed73d --- /dev/null +++ b/common/src/api/java/dev/engine_room/flywheel/api/visual/ShaderLightVisual.java @@ -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. + * + *

Sections passed into {@link SectionCollector#sections} will have their light data handed to the + * backend and queryable by {@code flw_light*} functions in shaders. + *
+ * 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 { + +} diff --git a/common/src/api/java/dev/engine_room/flywheel/api/visual/Visual.java b/common/src/api/java/dev/engine_room/flywheel/api/visual/Visual.java index 4aed57f59..f6d44d1de 100644 --- a/common/src/api/java/dev/engine_room/flywheel/api/visual/Visual.java +++ b/common/src/api/java/dev/engine_room/flywheel/api/visual/Visual.java @@ -5,7 +5,8 @@ * * @see DynamicVisual * @see TickableVisual - * @see LitVisual + * @see LightUpdatedVisual + * @see ShaderLightVisual */ public interface Visual { /** diff --git a/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualEmbedding.java b/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualEmbedding.java index f935ab006..baf35844c 100644 --- a/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualEmbedding.java +++ b/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualEmbedding.java @@ -4,7 +4,6 @@ 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. - * - *

Call this method on as many or as few boxes as you need to - * encompass all child visuals of this embedding.

- * - *

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.

- * - * @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. * diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java b/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java index 001f8f39b..59e255cd5 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java @@ -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")); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/LightUpdateHolder.java b/common/src/backend/java/dev/engine_room/flywheel/backend/LightUpdateHolder.java new file mode 100644 index 000000000..e6f9e856d --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/LightUpdateHolder.java @@ -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 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); + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/Samplers.java b/common/src/backend/java/dev/engine_room/flywheel/backend/Samplers.java index d8d2975dc..4441c6246 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/Samplers.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/Samplers.java @@ -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; } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/ContextShader.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/ContextShader.java index bcc7832e5..7c8aac5dd 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/ContextShader.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/ContextShader.java @@ -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; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/Pipelines.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/Pipelines.java index 6b3af1ff9..65f122f4b 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/Pipelines.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/Pipelines.java @@ -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() diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/SsboInstanceComponent.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/SsboInstanceComponent.java index 756ab202f..c8902b2d4 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/SsboInstanceComponent.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/component/SsboInstanceComponent.java @@ -5,7 +5,7 @@ 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 @@ protected void generateUnpacking(GlslBuilder builder) { 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(); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java index 56fbfcf9d..6cbadf9c9 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/AbstractInstancer.java @@ -25,8 +25,6 @@ public abstract class AbstractInstancer implements Instancer protected AbstractInstancer(InstanceType type, Environment environment) { this.type = type; this.environment = environment; - - environment.acquire(); } @Override @@ -177,9 +175,7 @@ public void clear() { deleted.clear(); } - public void delete() { - environment.release(); - } + public abstract void delete(); @Override public String toString() { diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/Arena.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/Arena.java new file mode 100644 index 000000000..101cc0013 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/Arena.java @@ -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; + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java index 014d2ccdf..8b78be928 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/DrawManager.java @@ -45,7 +45,7 @@ public void delete() { 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) { diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java index 3e7dd9d61..46e66c007 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java @@ -14,34 +14,46 @@ 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> 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> drawManager, int maxOriginDistance) { + public EngineImpl(LevelAccessor level, DrawManager> 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 createFramePlan() { - return SyncedPlan.of(this::flush); + return lightStorage.createFramePlan() + .then(SyncedPlan.of(this::flush)); } @Override @@ -62,11 +74,6 @@ public void renderCrumbling(TaskExecutor executor, RenderContext context, List Instancer instancer(Environment environment, InstanceType type, Model model, RenderStage stage) { @@ -102,8 +114,8 @@ public Instancer instancer(Environment environment, Inst 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 EnvironmentStorage environmentStorage() { 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 Vec3i renderOrigin() { @Override public VisualEmbedding createEmbedding() { - var out = new TopLevelEmbeddedEnvironment(EngineImpl.this, stage); + var out = new EmbeddedEnvironment(EngineImpl.this, stage); environmentStorage.track(out); return out; } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EnvironmentStorage.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EnvironmentStorage.java deleted file mode 100644 index c6500069f..000000000 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EnvironmentStorage.java +++ /dev/null @@ -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 environments = ReferenceSets.synchronize(new ReferenceLinkedOpenHashSet<>()); - private final Queue 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(); - } -} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightLut.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightLut.java new file mode 100644 index 000000000..bb04c7760 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightLut.java @@ -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>> buildIndices(Long2IntMap sectionIndicesMaps, LongArrayList positions, int baseX) { + final var indices = new ReferenceArrayList>>(); + 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>> 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; + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightStorage.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightStorage.java new file mode 100644 index 000000000..909dbdb95 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/LightStorage.java @@ -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. + * + *

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. + * + *

Sections are logically stored as a 9x9x9 array of longs, + * where each long holds a 2x2x2 array of light data. + *
Both the greater array and the longs are packed in x, z, y order. + * + *

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. + *

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 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. + *

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; + } + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/AbstractEmbeddedEnvironment.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedEnvironment.java similarity index 64% rename from common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/AbstractEmbeddedEnvironment.java rename to common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedEnvironment.java index 07adb4805..fef623172 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/AbstractEmbeddedEnvironment.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedEnvironment.java @@ -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.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; + + 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 AbstractEmbeddedEnvironment(EngineImpl engine, RenderStage renderStage) { + public EmbeddedEnvironment(EngineImpl engine, RenderStage renderStage, @Nullable EmbeddedEnvironment parent) { this.engine = engine; this.renderStage = renderStage; + this.parent = parent; instancerProvider = new InstancerProvider() { @Override public Instancer instancer(InstanceType 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 void transforms(Matrix4fc pose, Matrix3fc normal) { 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 Vec3i renderOrigin() { @Override public VisualEmbedding createEmbedding() { - var out = new NestedEmbeddedEnvironment(this, engine, renderStage); + var out = new EmbeddedEnvironment(engine, renderStage, this); engine.environmentStorage() .track(out); return out; } - /** - * 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(); + 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); } - /** - * Called when referenceCount goes to 0 - */ @Override - public void _delete() { - engine.environmentStorage().enqueueDeletion(this); + 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); } - public abstract void setupLight(GlProgram program); + 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 abstract void composeMatrices(Matrix4f pose, Matrix3f normal); + public boolean isDeleted() { + return deleted; + } /** - * Called in EnvironmentStorage#flush + * Called by visuals */ - public abstract void actuallyDelete(); + @Override + public void delete() { + deleted = true; + } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedLightTexture.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedLightTexture.java deleted file mode 100644 index 7e4325fb9..000000000 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedLightTexture.java +++ /dev/null @@ -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(); - } - } -} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedLightVolume.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedLightVolume.java deleted file mode 100644 index c815ae3ea..000000000 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddedLightVolume.java +++ /dev/null @@ -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(); - } -} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddingUniforms.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddingUniforms.java index eb0c58bef..e62f0b018 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddingUniforms.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EmbeddingUniforms.java @@ -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() { + } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/Environment.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/Environment.java index 91da73062..83fa60d62 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/Environment.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/Environment.java @@ -6,11 +6,7 @@ public interface Environment { ContextShader contextShader(); - void setupDraw(GlProgram drawProgram); - void setupCull(GlProgram cullProgram); - void acquire(); - - void release(); + void setupDraw(GlProgram drawProgram); } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EnvironmentStorage.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EnvironmentStorage.java new file mode 100644 index 000000000..9206b9cf3 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/EnvironmentStorage.java @@ -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 environments = ReferenceSets.synchronize(new ReferenceLinkedOpenHashSet<>()); + + public void track(EmbeddedEnvironment environment) { + environments.add(environment); + } + + public void flush() { + environments.removeIf(EmbeddedEnvironment::isDeleted); + environments.forEach(EmbeddedEnvironment::flush); + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/GlobalEnvironment.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/GlobalEnvironment.java index b4c852280..aaac1ca35 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/GlobalEnvironment.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/GlobalEnvironment.java @@ -14,23 +14,12 @@ public ContextShader contextShader() { 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) { } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/NestedEmbeddedEnvironment.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/NestedEmbeddedEnvironment.java deleted file mode 100644 index f957a7e43..000000000 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/NestedEmbeddedEnvironment.java +++ /dev/null @@ -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(); - } -} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/TopLevelEmbeddedEnvironment.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/TopLevelEmbeddedEnvironment.java deleted file mode 100644 index d5a8aa9f9..000000000 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/embed/TopLevelEmbeddedEnvironment.java +++ /dev/null @@ -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(); - } -} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/BufferBindings.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/BufferBindings.java new file mode 100644 index 000000000..a0ae93a28 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/BufferBindings.java @@ -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() { + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java index fff5d9ea1..90ecd4149 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectBuffers.java @@ -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 void bindForDraw() { 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 @@ private void multiBind() { */ 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() { diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java index c4d5fb8ea..b79a52edd 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java @@ -280,6 +280,16 @@ public void delete() { 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) { diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java index 8da0093f9..d9ef2254a 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java @@ -20,6 +20,7 @@ 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> { private final GlVertexArray vertexArray; private final Map, 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); + stagingBuffer = new StagingBuffer(this.programs); meshPool = new MeshPool(); - vertexArray = GlVertexArray.create(); - meshPool.bind(vertexArray); + lightBuffers = new LightBuffers(); } @Override @@ -83,6 +84,7 @@ public void renderStage(RenderStage stage) { TextureBinder.bindLightAndOverlay(); vertexArray.bindForDraw(); + lightBuffers.bind(); Uniforms.bindAll(); for (var group : cullingGroups.values()) { @@ -95,19 +97,25 @@ public void renderStage(RenderStage stage) { } @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 void renderCrumbling(List crumblingBlocks) { 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(); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java index 38099c85d..b0ac9bc98 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectInstancer.java @@ -125,8 +125,6 @@ private void uploadAllModelIndices(StagingBuffer stagingBuffer, long modelIndexB @Override public void delete() { - super.delete(); - for (IndirectDraw draw : draws()) { draw.delete(); } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/LightBuffers.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/LightBuffers.java new file mode 100644 index 000000000..9db63afb0 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/LightBuffers.java @@ -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()); + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java index b532823ab..ae231de0b 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedDrawManager.java @@ -18,6 +18,7 @@ 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> { 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 InstancedDrawManager(InstancingPrograms programs) { 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 void flush() { } meshPool.flush(); + + light.flush(lightStorage); } @Override @@ -91,6 +96,7 @@ public void renderStage(RenderStage stage) { Uniforms.bindAll(); vao.bindForDraw(); TextureBinder.bindLightAndOverlay(); + light.bind(); drawSet.draw(instanceTexture, programs); @@ -113,6 +119,8 @@ public void delete() { programs.release(); vao.delete(); + light.delete(); + super.delete(); } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java index 9ca3d5cc8..a5dff27d9 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedInstancer.java @@ -116,8 +116,6 @@ public boolean needsToGrow(long capacity) { } public void delete() { - super.delete(); - if (vbo == null) { return; } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedLight.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedLight.java new file mode 100644 index 000000000..7831019d1 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/instancing/InstancedLight.java @@ -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(); + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/uniform/DebugMode.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/uniform/DebugMode.java index 8eae9c292..dcc13dc4c 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/uniform/DebugMode.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/uniform/DebugMode.java @@ -14,7 +14,6 @@ public enum DebugMode implements StringRepresentable { LIGHT_COLOR, OVERLAY, DIFFUSE, - LIGHT_VOLUME, ; public static final Codec CODEC = StringRepresentable.fromEnum(DebugMode::values); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/gl/TextureBuffer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/gl/TextureBuffer.java index 21eb9361f..d7bbc9cfc 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/gl/TextureBuffer.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/gl/TextureBuffer.java @@ -5,14 +5,20 @@ 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 diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/ClientChunkCacheMixin.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/ClientChunkCacheMixin.java new file mode 100644 index 000000000..eb8c974ec --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/ClientChunkCacheMixin.java @@ -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()); + } +} diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.frag b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.frag index dc8334916..af16b80e2 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.frag +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.frag @@ -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; diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.glsl new file mode 100644 index 000000000..a839a81c2 --- /dev/null +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.glsl @@ -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); diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert index d9934d42e..d1e751929 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert @@ -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; diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag index f73846dea..23df5b7e9 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag @@ -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); diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.vert index ec009e414..075dae3f6 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.vert +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.vert @@ -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); diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/buffer_bindings.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/buffer_bindings.glsl index 4aa9edc56..c37db3502 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/buffer_bindings.glsl +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/buffer_bindings.glsl @@ -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 diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/light.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/light.glsl new file mode 100644 index 000000000..48975050e --- /dev/null +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/light.glsl @@ -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]; +} diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.frag b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.frag index 334b7b461..b1dbb0822 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.frag +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.frag @@ -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; diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.vert index 55bd8fc0f..a95d31662 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.vert +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/main.vert @@ -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[]; diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/light.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/light.glsl new file mode 100644 index 000000000..f023322a4 --- /dev/null +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/light.glsl @@ -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; +} diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.frag b/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.frag index 661c3a769..89b09b37f 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.frag +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.frag @@ -1,4 +1,5 @@ #include "flywheel:internal/common.frag" +#include "flywheel:internal/instancing/light.glsl" uniform uvec4 _flw_packedMaterial; diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.vert index 8c4b9d6a3..4b6914eae 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.vert +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/instancing/main.vert @@ -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; diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl new file mode 100644 index 000000000..4d864928c --- /dev/null +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/light_lut.glsl @@ -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; +} + diff --git a/common/src/backend/resources/flywheel.backend.mixins.json b/common/src/backend/resources/flywheel.backend.mixins.json index 2d924bef0..e303f7b03 100644 --- a/common/src/backend/resources/flywheel.backend.mixins.json +++ b/common/src/backend/resources/flywheel.backend.mixins.json @@ -6,6 +6,7 @@ "refmap": "backend-flywheel.refmap.json", "client": [ "AbstractClientPlayerAccessor", + "ClientChunkCacheMixin", "GlStateManagerMixin", "LevelRendererAccessor", "OptionsMixin", diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/box/Box.java b/common/src/lib/java/dev/engine_room/flywheel/lib/box/Box.java deleted file mode 100644 index 3955cde1b..000000000 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/box/Box.java +++ /dev/null @@ -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); - } -} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/box/MutableBox.java b/common/src/lib/java/dev/engine_room/flywheel/lib/box/MutableBox.java deleted file mode 100644 index db69a3883..000000000 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/box/MutableBox.java +++ /dev/null @@ -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 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 + ')'; - } -} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/light/LightPacking.java b/common/src/lib/java/dev/engine_room/flywheel/lib/light/LightPacking.java deleted file mode 100644 index 7cd9ce37e..000000000 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/light/LightPacking.java +++ /dev/null @@ -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; - } -} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/light/LightVolume.java b/common/src/lib/java/dev/engine_room/flywheel/lib/light/LightVolume.java deleted file mode 100644 index ab71bcae3..000000000 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/light/LightVolume.java +++ /dev/null @@ -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(); - } -} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/AbstractBlockEntityVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/AbstractBlockEntityVisual.java index 59722198e..a3fafb59f 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/AbstractBlockEntityVisual.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/AbstractBlockEntityVisual.java @@ -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 @@ *

    *
  • {@link DynamicVisual}
  • *
  • {@link TickableVisual}
  • + *
  • {@link LightUpdatedVisual}
  • + *
  • {@link ShaderLightVisual}
  • *
* See the interfaces' documentation for more information about each one. * @@ -34,13 +36,13 @@ * * @param The type of {@link BlockEntity}. */ -public abstract class AbstractBlockEntityVisual extends AbstractVisual implements BlockEntityVisual, LitVisual { +public abstract class AbstractBlockEntityVisual extends AbstractVisual implements BlockEntityVisual, 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 AbstractBlockEntityVisual(VisualizationContext ctx, T blockEntity, float } @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))); } /** diff --git a/common/src/lib/resources/assets/flywheel/flywheel/material/default.frag b/common/src/lib/resources/assets/flywheel/flywheel/material/default.frag index 68dfe9a5b..f072c1849 100644 --- a/common/src/lib/resources/assets/flywheel/flywheel/material/default.frag +++ b/common/src/lib/resources/assets/flywheel/flywheel/material/default.frag @@ -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 } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/ClientChunkCacheMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/ClientChunkCacheMixin.java index d12415179..f55d6d2e9 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/ClientChunkCacheMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/ClientChunkCacheMixin.java @@ -24,7 +24,7 @@ abstract class ClientChunkCacheMixin { var manager = VisualizationManagerImpl.get(level); if (manager != null) { - manager.enqueueLightUpdateSection(pos.asLong()); + manager.onLightUpdate(pos.asLong()); } } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizationManagerImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizationManagerImpl.java index e0accaf2d..ccf69cc0e 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizationManagerImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizationManagerImpl.java @@ -45,6 +45,7 @@ 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; @@ -104,6 +105,15 @@ private VisualizationManagerImpl(LevelAccessor level) { .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)); @@ -292,6 +302,12 @@ public void renderCrumbling(RenderContext context, Long2ObjectMap createRaw(BlockEntity obj, float partialTick) { @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(); } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/manager/VisualManagerImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/manager/VisualManagerImpl.java index 3abc3c7ac..f0d3420bb 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/manager/VisualManagerImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/manager/VisualManagerImpl.java @@ -10,6 +10,7 @@ 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> implements VisualManager { private final Queue> queue = new ConcurrentLinkedQueue<>(); @@ -53,10 +54,6 @@ public void queueUpdate(T obj) { queue.add(Transaction.update(obj)); } - public void invalidate() { - getStorage().invalidate(); - } - public void processQueue(float partialTick) { var storage = getStorage(); Transaction transaction; @@ -74,4 +71,23 @@ public Plan tickPlan() { return SimplePlan.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(); + } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/LitVisualStorage.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/LightUpdatedVisualStorage.java similarity index 63% rename from common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/LitVisualStorage.java rename to common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/LightUpdatedVisualStorage.java index 3651fe278..3f730abc0 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/LitVisualStorage.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/LightUpdatedVisualStorage.java @@ -10,13 +10,12 @@ 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 @@ /** * 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 visuals2Sections = new WeakHashMap<>(); - private final Long2ObjectMap> sections2Visuals = new Long2ObjectOpenHashMap<>(); + private final Map visual2Sections = new WeakHashMap<>(); + private final Long2ObjectMap> section2Updaters = new Long2ObjectOpenHashMap<>(); - private final Queue movedVisuals = new ConcurrentLinkedQueue<>(); private final LongSet sectionsUpdatedThisFrame = new LongOpenHashSet(); + private final Queue movedVisuals = new ConcurrentLinkedQueue<>(); private long updateId = INITIAL_UPDATE_ID; @@ -54,9 +53,9 @@ public Plan plan() { 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 Plan plan() { } 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 @@ private long getNextUpdateId() { 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)); - public void setNotifierAndAdd(LitVisual visual) { - visual.setLightSectionNotifier(new LitVisualNotifierImpl(visual)); - add(visual); + addInner(visual, tracker); } - private void add(LitVisual visual) { - LongSet sections = new LongArraySet(); - - visual.collectLightSections(sections::add); + 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()); - // 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 listeners = sections2Visuals.get(section); - if (listeners != null) { - listeners.remove(indexOfUpdater(listeners, visual)); + List updaters = section2Updaters.get(section); + if (updaters != null) { + updaters.remove(indexOfUpdater(updaters, visual)); } } return true; } + public void onLightUpdate(long section) { + sectionsUpdatedThisFrame.add(section); + } + public void clear() { - visuals2Sections.clear(); - sections2Visuals.clear(); + visual2Sections.clear(); + section2Updaters.clear(); sectionsUpdatedThisFrame.clear(); + movedVisuals.clear(); } - private static int indexOfUpdater(List listeners, LitVisual visual) { - for (int i = 0; i < listeners.size(); i++) { - if (listeners.get(i) + private static int indexOfUpdater(List updaters, LightUpdatedVisual visual) { + for (int i = 0; i < updaters.size(); i++) { + if (updaters.get(i) .visual() == visual) { return i; } @@ -159,7 +155,7 @@ private static int indexOfUpdater(List listeners, LitVisual visual) { 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 @@ private static Updater createUpdater(LitVisual visual, int sectionCount) { } // 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 void updateLight(Context ctx) { // 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 @@ record Context(long updateId, float partialTick) { } } - 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) { + } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/SectionTracker.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/SectionTracker.java new file mode 100644 index 000000000..9cba32f69 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/SectionTracker.java @@ -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 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); + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/ShaderLightVisualStorage.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/ShaderLightVisualStorage.java new file mode 100644 index 000000000..052a9e06c --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/ShaderLightVisualStorage.java @@ -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 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(); + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/Storage.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/Storage.java index b599dc26e..4e2d1ebaf 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/Storage.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/storage/Storage.java @@ -10,7 +10,9 @@ 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; @@ -23,13 +25,14 @@ public abstract class Storage { protected final Supplier visualizationContextSupplier; + + private final Map visuals = new Reference2ObjectOpenHashMap<>(); protected final PlanMap dynamicVisuals = new PlanMap<>(); protected final PlanMap tickableVisuals = new PlanMap<>(); protected final List simpleDynamicVisuals = new ArrayList<>(); protected final List simpleTickableVisuals = new ArrayList<>(); - protected final LitVisualStorage litVisuals = new LitVisualStorage(); - - private final Map visuals = new Reference2ObjectOpenHashMap<>(); + protected final LightUpdatedVisualStorage lightUpdatedVisuals = new LightUpdatedVisualStorage(); + protected final ShaderLightVisualStorage shaderLightVisuals = new ShaderLightVisualStorage(); public Storage(Supplier visualizationContextSupplier) { this.visualizationContextSupplier = visualizationContextSupplier; @@ -39,6 +42,29 @@ public Collection getAllVisuals() { return visuals.values(); } + public Plan framePlan() { + return NestedPlan.of(dynamicVisuals, lightUpdatedVisuals.plan(), ForEachPlan.of(() -> simpleDynamicVisuals, SimpleDynamicVisual::beginFrame)); + } + + public Plan tickPlan() { + return NestedPlan.of(tickableVisuals, ForEachPlan.of(() -> simpleTickableVisuals, SimpleTickableVisual::tick)); + } + + public LightUpdatedVisualStorage lightUpdatedVisuals() { + return lightUpdatedVisuals; + } + + public ShaderLightVisualStorage shaderLightVisuals() { + return shaderLightVisuals; + } + + /** + * Is the given object currently capable of being added? + * + * @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); @@ -54,6 +80,13 @@ public void remove(T obj) { 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); @@ -61,16 +94,13 @@ public void remove(T obj) { tickableVisuals.remove(tickable); } } - if (visual instanceof DynamicVisual dynamic) { - if (visual instanceof SimpleDynamicVisual simpleDynamic) { - simpleDynamicVisuals.remove(simpleDynamic); - } else { - dynamicVisuals.remove(dynamic); - } + if (visual instanceof LightUpdatedVisual lightUpdated) { + lightUpdatedVisuals.remove(lightUpdated); } - if (visual instanceof LitVisual lit) { - litVisuals.remove(lit); + if (visual instanceof ShaderLightVisual shaderLight) { + shaderLightVisuals.remove(shaderLight); } + visual.delete(); } @@ -85,11 +115,13 @@ public void update(T obj, float partialTick) { } public void recreateAll(float partialTick) { - tickableVisuals.clear(); dynamicVisuals.clear(); - simpleTickableVisuals.clear(); + tickableVisuals.clear(); simpleDynamicVisuals.clear(); - litVisuals.clear(); + simpleTickableVisuals.clear(); + lightUpdatedVisuals.clear(); + shaderLightVisuals.clear(); + visuals.replaceAll((obj, visual) -> { visual.delete(); @@ -103,15 +135,6 @@ public void recreateAll(float partialTick) { }); } - public void invalidate() { - tickableVisuals.clear(); - dynamicVisuals.clear(); - litVisuals.clear(); - visuals.values() - .forEach(Visual::delete); - visuals.clear(); - } - private void create(T obj, float partialTick) { var visual = createRaw(obj, partialTick); @@ -124,19 +147,15 @@ private void create(T obj, float partialTick) { @Nullable protected abstract Visual createRaw(T obj, float partialTick); - public Plan framePlan() { - return NestedPlan.of(dynamicVisuals, litVisuals.plan(), ForEachPlan.of(() -> simpleDynamicVisuals, SimpleDynamicVisual::beginFrame)); - } - - public Plan tickPlan() { - return NestedPlan.of(tickableVisuals, ForEachPlan.of(() -> simpleTickableVisuals, SimpleTickableVisual::tick)); - } - - public void enqueueLightUpdateSection(long section) { - litVisuals.enqueueLightUpdateSection(section); - } - private void setup(Visual visual) { + 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); @@ -145,23 +164,31 @@ private void setup(Visual visual) { } } - if (visual instanceof DynamicVisual dynamic) { - if (visual instanceof SimpleDynamicVisual simpleDynamic) { - simpleDynamicVisuals.add(simpleDynamic); - } else { - dynamicVisuals.add(dynamic, dynamic.planFrame()); + 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); } - } - if (visual instanceof LitVisual lit) { - litVisuals.setNotifierAndAdd(lit); + if (visual instanceof ShaderLightVisual shaderLight) { + shaderLightVisuals.add(shaderLight, tracker); + } } } - /** - * Is the given object currently capable of being added? - * - * @return true if the object is currently capable of being visualized. - */ - public abstract boolean willAccept(T obj); + public void invalidate() { + dynamicVisuals.clear(); + tickableVisuals.clear(); + simpleDynamicVisuals.clear(); + simpleTickableVisuals.clear(); + lightUpdatedVisuals.clear(); + shaderLightVisuals.clear(); + visuals.values() + .forEach(Visual::delete); + visuals.clear(); + } } diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/MinecartVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/MinecartVisual.java index b0a72f0a7..873ff2bce 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/MinecartVisual.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/MinecartVisual.java @@ -194,7 +194,7 @@ private void updateInstances(float partialTick) { body.setTransform(stack) .setChanged(); - // TODO: Use LitVisual if possible. + // TODO: Use LightUpdatedVisual/ShaderLightVisual if possible. updateLight(partialTick); } diff --git a/docs/shader-api/common.glsl b/docs/shader-api/common.glsl new file mode 100644 index 000000000..9362c693d --- /dev/null +++ b/docs/shader-api/common.glsl @@ -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); diff --git a/docs/shader-api/fragment.glsl b/docs/shader-api/fragment.glsl index a99837c25..e13ec6588 100644 --- a/docs/shader-api/fragment.glsl +++ b/docs/shader-api/fragment.glsl @@ -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; diff --git a/docs/shader-api/vertex.glsl b/docs/shader-api/vertex.glsl index 338923540..5dcab38e7 100644 --- a/docs/shader-api/vertex.glsl +++ b/docs/shader-api/vertex.glsl @@ -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();