From e8e89f022b99077bb5a37e3dd38570278f70bb7e Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Mon, 26 May 2025 16:10:01 +1000 Subject: [PATCH] Wip on geometry cache --- .../voxy/client/core/VoxyRenderSystem.java | 4 +- .../client/core/rendering/GeometryCache.java | 20 +++++++++- .../client/core/rendering/RenderService.java | 11 ++---- .../core/rendering/SectionUpdateRouter.java | 6 ++- .../building/RenderGenerationService.java | 23 +++++++---- .../hierachical/AsyncNodeManager.java | 39 ++++++++++++++++--- 6 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java index 3b7c4d15..df3e1daf 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java @@ -346,8 +346,8 @@ public class VoxyRenderSystem { private void testFullMesh() { var modelService = new ModelBakerySubsystem(this.worldIn.getMapper()); var completedCounter = new AtomicInteger(); - var generationService = new RenderGenerationService(this.worldIn, modelService, VoxyCommon.getInstance().getThreadPool(), a-> {completedCounter.incrementAndGet(); a.free();}, false); - + var generationService = new RenderGenerationService(this.worldIn, modelService, VoxyCommon.getInstance().getThreadPool(), false); + generationService.setResultConsumer(a-> {completedCounter.incrementAndGet(); a.free();}); var r = new Random(12345); { diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/GeometryCache.java b/src/main/java/me/cortex/voxy/client/core/rendering/GeometryCache.java index a7c8d0a8..0ae80f37 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/GeometryCache.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/GeometryCache.java @@ -3,8 +3,12 @@ package me.cortex.voxy.client.core.rendering; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import me.cortex.voxy.client.core.rendering.building.BuiltSection; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.StampedLock; + //CPU side cache for section geometry, not thread safe public class GeometryCache { + private final ReentrantLock lock = new ReentrantLock(); private long maxCombinedSize; private long currentSize; private final Long2ObjectLinkedOpenHashMap cache = new Long2ObjectLinkedOpenHashMap<>(); @@ -18,25 +22,37 @@ public class GeometryCache { //Puts the section into the cache public void put(BuiltSection section) { + this.lock.lock(); var prev = this.cache.put(section.position, section); this.currentSize += section.geometryBuffer.size; if (prev != null) { this.currentSize -= prev.geometryBuffer.size; - prev.free(); } - while (this.maxCombinedSize <= this.currentSize) { var entry = this.cache.removeFirst(); this.currentSize -= entry.geometryBuffer.size; entry.free(); } + this.lock.unlock(); + if (prev != null) { + prev.free(); + } } public BuiltSection remove(long position) { + this.lock.lock(); var section = this.cache.remove(position); if (section != null) { this.currentSize -= section.geometryBuffer.size; } + this.lock.unlock(); return section; } + + public void clear(long position) { + var sec = this.remove(position); + if (sec != null) { + sec.free(); + } + } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java index ab0c9dde..e6d1e821 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java @@ -75,21 +75,18 @@ public class RenderService, J extends Vi Logger.info("Using renderer: " + this.sectionRenderer.getClass().getSimpleName() + " with geometry buffer of: " + geometryCapacity + " bytes"); //Do something incredibly hacky, we dont need to keep the reference to this around, so just connect and discard - var router = new SectionUpdateRouter(); - - this.nodeManager = new AsyncNodeManager(1<<21, router, this.geometryData); - this.nodeCleaner = new NodeCleaner(this.nodeManager); this.viewportSelector = new ViewportSelector<>(this.sectionRenderer::createViewport); this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool, - this.nodeManager::submitGeometryResult, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets, + this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets, ()->true); - router.setCallbacks(this.renderGen::enqueueTask, this.nodeManager::submitChildChange); + this.nodeManager = new AsyncNodeManager(1<<21, this.geometryData, this.renderGen); + this.nodeCleaner = new NodeCleaner(this.nodeManager); this.traversal = new HierarchicalOcclusionTraverser(this.nodeManager, this.nodeCleaner); - world.setDirtyCallback(router::forwardEvent); + world.setDirtyCallback(this.nodeManager::worldEvent); Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome); world.getMapper().setBiomeCallback(this.modelService::addBiome); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java b/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java index 87c5b6bc..7f6d9218 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java @@ -22,13 +22,15 @@ public class SectionUpdateRouter implements ISectionWatcher { } } + private LongConsumer initialRenderMeshGen; private LongConsumer renderMeshGen; private IChildUpdate childUpdateCallback; - public void setCallbacks(LongConsumer renderMeshGen, IChildUpdate childUpdateCallback) { + public void setCallbacks(LongConsumer initialRenderMeshGen, LongConsumer renderMeshGen, IChildUpdate childUpdateCallback) { if (this.renderMeshGen != null) { throw new IllegalStateException(); } + this.initialRenderMeshGen = initialRenderMeshGen; this.renderMeshGen = renderMeshGen; this.childUpdateCallback = childUpdateCallback; } @@ -69,7 +71,7 @@ public class SectionUpdateRouter implements ISectionWatcher { } if ((delta&UPDATE_TYPE_BLOCK_BIT)!=0) { //If we added it, immediately invoke for an update - this.renderMeshGen.accept(position); + this.initialRenderMeshGen.accept(position); } return delta!=0; } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java index 08693405..237c72e0 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java @@ -57,21 +57,20 @@ public class RenderGenerationService { private final WorldEngine world; private final ModelBakerySubsystem modelBakery; - private final Consumer resultConsumer; + private Consumer resultConsumer; private final boolean emitMeshlets; private final ServiceSlice threads; - public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer consumer, boolean emitMeshlets) { - this(world, modelBakery, serviceThreadPool, consumer, emitMeshlets, ()->true); + public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, boolean emitMeshlets) { + this(world, modelBakery, serviceThreadPool, emitMeshlets, ()->true); } - public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer consumer, boolean emitMeshlets, BooleanSupplier taskLimiter) { + public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, boolean emitMeshlets, BooleanSupplier taskLimiter) { this.emitMeshlets = emitMeshlets; this.world = world; this.modelBakery = modelBakery; - this.resultConsumer = consumer; this.threads = serviceThreadPool.createService("Section mesh generation service", 100, ()->{ //Thread local instance of the factory @@ -83,6 +82,10 @@ public class RenderGenerationService { }, taskLimiter); } + public void setResultConsumer(Consumer consumer) { + this.resultConsumer = consumer; + } + //NOTE: the biomes are always fully populated/kept up to date //Asks the Model system to bake all blocks that currently dont have a model @@ -139,7 +142,9 @@ public class RenderGenerationService { } if (section == null) { - this.resultConsumer.accept(BuiltSection.empty(task.position)); + if (this.resultConsumer != null) { + this.resultConsumer.accept(BuiltSection.empty(task.position)); + } return; } section.assertNotFree(); @@ -265,7 +270,11 @@ public class RenderGenerationService { } if (mesh != null) {//If the mesh is null it means it didnt finish, so dont submit - this.resultConsumer.accept(mesh); + if (this.resultConsumer != null) { + this.resultConsumer.accept(mesh); + } else { + mesh.free(); + } } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java index fa9a7c53..b9869718 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java @@ -7,8 +7,9 @@ import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.gl.shader.Shader; import me.cortex.voxy.client.core.gl.shader.ShaderType; import me.cortex.voxy.client.core.rendering.GeometryCache; -import me.cortex.voxy.client.core.rendering.ISectionWatcher; +import me.cortex.voxy.client.core.rendering.SectionUpdateRouter; import me.cortex.voxy.client.core.rendering.building.BuiltSection; +import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; import me.cortex.voxy.client.core.rendering.section.geometry.BasicAsyncGeometryManager; import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData; @@ -16,6 +17,7 @@ import me.cortex.voxy.client.core.rendering.util.UploadStream; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.AllocationArena; import me.cortex.voxy.common.util.MemoryBuffer; +import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldSection; import org.lwjgl.system.MemoryUtil; @@ -61,6 +63,7 @@ public class AsyncNodeManager { private final NodeManager manager; private final BasicAsyncGeometryManager geometryManager; private final IGeometryData geometryData; + private final SectionUpdateRouter router; private final GeometryCache geometryCache = new GeometryCache(1L<<32); @@ -77,7 +80,7 @@ public class AsyncNodeManager { private boolean needsWaitForSync = false; - public AsyncNodeManager(int maxNodeCount, ISectionWatcher watcher, IGeometryData geometryData) { + public AsyncNodeManager(int maxNodeCount, IGeometryData geometryData, RenderGenerationService renderService) { //Note the current implmentation of ISectionWatcher is threadsafe //Note: geometry data is the data store/source, not the management, it is just a raw store of data // it MUST ONLY be accessed on the render thread @@ -99,7 +102,20 @@ public class AsyncNodeManager { this.thread.setName("Async Node Manager"); this.geometryManager = new BasicAsyncGeometryManager(((BasicSectionGeometryData)geometryData).getMaxSectionCount(), this.geometryCapacity); - this.manager = new NodeManager(maxNodeCount, this.geometryManager, watcher); + + this.router = new SectionUpdateRouter(); + this.router.setCallbacks(pos->{//On initial render gen, try get from geometry cache + var cachedGeometry = this.geometryCache.remove(pos); + if (cachedGeometry != null) {//Use the cached geometry + this.submitGeometryResult(cachedGeometry); + } else {//Else we need to request it + renderService.enqueueTask(pos); + } + }, renderService::enqueueTask, this::submitChildChange); + renderService.setResultConsumer(this::submitGeometryResult); + + this.manager = new NodeManager(maxNodeCount, this.geometryManager, this.router); + //Dont do the move... is just to much effort this.manager.setClear(new NodeManager.ICleaner() { @Override @@ -273,6 +289,12 @@ public class AsyncNodeManager { continue; } + if (pos == 0) { + //THIS SHOULD BE IMPOSSIBLE + //TODO: VVVVV MUCH MEGA FIX + continue; + } + this.manager.removeNodeGeometry(pos); } job.free(); @@ -600,7 +622,7 @@ public class AsyncNodeManager { this.addWork(); } - public void submitChildChange(WorldSection section) { + private void submitChildChange(WorldSection section) { if (!this.running) { return; } @@ -609,7 +631,7 @@ public class AsyncNodeManager { this.addWork(); } - public void submitGeometryResult(BuiltSection geometry) { + private void submitGeometryResult(BuiltSection geometry) { if (!this.running) { geometry.free(); return; @@ -724,6 +746,13 @@ public class AsyncNodeManager { return this.workCounter.get()!=0 && RESULT_HANDLE.get(this) != null; } + public void worldEvent(WorldSection section, int flags) { + //If there is any change, we need to clear the geometry cache before emitting update + this.geometryCache.clear(section.key); + + this.router.forwardEvent(section, flags); + } + //Results object, which is to be synced between the render thread and worker thread private static final class SyncResults { //Contains