Wip on geometry cache

This commit is contained in:
mcrcortex
2025-05-26 16:10:01 +10:00
parent 5d8cc2b3c4
commit e8e89f022b
6 changed files with 78 additions and 25 deletions

View File

@@ -346,8 +346,8 @@ public class VoxyRenderSystem {
private void testFullMesh() { private void testFullMesh() {
var modelService = new ModelBakerySubsystem(this.worldIn.getMapper()); var modelService = new ModelBakerySubsystem(this.worldIn.getMapper());
var completedCounter = new AtomicInteger(); 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); var r = new Random(12345);
{ {

View File

@@ -3,8 +3,12 @@ package me.cortex.voxy.client.core.rendering;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import me.cortex.voxy.client.core.rendering.building.BuiltSection; 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 //CPU side cache for section geometry, not thread safe
public class GeometryCache { public class GeometryCache {
private final ReentrantLock lock = new ReentrantLock();
private long maxCombinedSize; private long maxCombinedSize;
private long currentSize; private long currentSize;
private final Long2ObjectLinkedOpenHashMap<BuiltSection> cache = new Long2ObjectLinkedOpenHashMap<>(); private final Long2ObjectLinkedOpenHashMap<BuiltSection> cache = new Long2ObjectLinkedOpenHashMap<>();
@@ -18,25 +22,37 @@ public class GeometryCache {
//Puts the section into the cache //Puts the section into the cache
public void put(BuiltSection section) { public void put(BuiltSection section) {
this.lock.lock();
var prev = this.cache.put(section.position, section); var prev = this.cache.put(section.position, section);
this.currentSize += section.geometryBuffer.size; this.currentSize += section.geometryBuffer.size;
if (prev != null) { if (prev != null) {
this.currentSize -= prev.geometryBuffer.size; this.currentSize -= prev.geometryBuffer.size;
prev.free();
} }
while (this.maxCombinedSize <= this.currentSize) { while (this.maxCombinedSize <= this.currentSize) {
var entry = this.cache.removeFirst(); var entry = this.cache.removeFirst();
this.currentSize -= entry.geometryBuffer.size; this.currentSize -= entry.geometryBuffer.size;
entry.free(); entry.free();
} }
this.lock.unlock();
if (prev != null) {
prev.free();
}
} }
public BuiltSection remove(long position) { public BuiltSection remove(long position) {
this.lock.lock();
var section = this.cache.remove(position); var section = this.cache.remove(position);
if (section != null) { if (section != null) {
this.currentSize -= section.geometryBuffer.size; this.currentSize -= section.geometryBuffer.size;
} }
this.lock.unlock();
return section; return section;
} }
public void clear(long position) {
var sec = this.remove(position);
if (sec != null) {
sec.free();
}
}
} }

View File

@@ -75,21 +75,18 @@ public class RenderService<T extends AbstractSectionRenderer<J, Q>, J extends Vi
Logger.info("Using renderer: " + this.sectionRenderer.getClass().getSimpleName() + " with geometry buffer of: " + geometryCapacity + " bytes"); 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 //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.viewportSelector = new ViewportSelector<>(this.sectionRenderer::createViewport);
this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool, this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool,
this.nodeManager::submitGeometryResult, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets,
()->true); ()->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); 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); Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome);
world.getMapper().setBiomeCallback(this.modelService::addBiome); world.getMapper().setBiomeCallback(this.modelService::addBiome);

View File

@@ -22,13 +22,15 @@ public class SectionUpdateRouter implements ISectionWatcher {
} }
} }
private LongConsumer initialRenderMeshGen;
private LongConsumer renderMeshGen; private LongConsumer renderMeshGen;
private IChildUpdate childUpdateCallback; private IChildUpdate childUpdateCallback;
public void setCallbacks(LongConsumer renderMeshGen, IChildUpdate childUpdateCallback) { public void setCallbacks(LongConsumer initialRenderMeshGen, LongConsumer renderMeshGen, IChildUpdate childUpdateCallback) {
if (this.renderMeshGen != null) { if (this.renderMeshGen != null) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
this.initialRenderMeshGen = initialRenderMeshGen;
this.renderMeshGen = renderMeshGen; this.renderMeshGen = renderMeshGen;
this.childUpdateCallback = childUpdateCallback; this.childUpdateCallback = childUpdateCallback;
} }
@@ -69,7 +71,7 @@ public class SectionUpdateRouter implements ISectionWatcher {
} }
if ((delta&UPDATE_TYPE_BLOCK_BIT)!=0) { if ((delta&UPDATE_TYPE_BLOCK_BIT)!=0) {
//If we added it, immediately invoke for an update //If we added it, immediately invoke for an update
this.renderMeshGen.accept(position); this.initialRenderMeshGen.accept(position);
} }
return delta!=0; return delta!=0;
} }

View File

@@ -57,21 +57,20 @@ public class RenderGenerationService {
private final WorldEngine world; private final WorldEngine world;
private final ModelBakerySubsystem modelBakery; private final ModelBakerySubsystem modelBakery;
private final Consumer<BuiltSection> resultConsumer; private Consumer<BuiltSection> resultConsumer;
private final boolean emitMeshlets; private final boolean emitMeshlets;
private final ServiceSlice threads; private final ServiceSlice threads;
public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer<BuiltSection> consumer, boolean emitMeshlets) { public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, boolean emitMeshlets) {
this(world, modelBakery, serviceThreadPool, consumer, emitMeshlets, ()->true); this(world, modelBakery, serviceThreadPool, emitMeshlets, ()->true);
} }
public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer<BuiltSection> consumer, boolean emitMeshlets, BooleanSupplier taskLimiter) { public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, boolean emitMeshlets, BooleanSupplier taskLimiter) {
this.emitMeshlets = emitMeshlets; this.emitMeshlets = emitMeshlets;
this.world = world; this.world = world;
this.modelBakery = modelBakery; this.modelBakery = modelBakery;
this.resultConsumer = consumer;
this.threads = serviceThreadPool.createService("Section mesh generation service", 100, ()->{ this.threads = serviceThreadPool.createService("Section mesh generation service", 100, ()->{
//Thread local instance of the factory //Thread local instance of the factory
@@ -83,6 +82,10 @@ public class RenderGenerationService {
}, taskLimiter); }, taskLimiter);
} }
public void setResultConsumer(Consumer<BuiltSection> consumer) {
this.resultConsumer = consumer;
}
//NOTE: the biomes are always fully populated/kept up to date //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 //Asks the Model system to bake all blocks that currently dont have a model
@@ -139,7 +142,9 @@ public class RenderGenerationService {
} }
if (section == null) { if (section == null) {
if (this.resultConsumer != null) {
this.resultConsumer.accept(BuiltSection.empty(task.position)); this.resultConsumer.accept(BuiltSection.empty(task.position));
}
return; return;
} }
section.assertNotFree(); 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 if (mesh != null) {//If the mesh is null it means it didnt finish, so dont submit
if (this.resultConsumer != null) {
this.resultConsumer.accept(mesh); this.resultConsumer.accept(mesh);
} else {
mesh.free();
}
} }
} }

View File

@@ -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.Shader;
import me.cortex.voxy.client.core.gl.shader.ShaderType; import me.cortex.voxy.client.core.gl.shader.ShaderType;
import me.cortex.voxy.client.core.rendering.GeometryCache; 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.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.BasicAsyncGeometryManager;
import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData;
import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData; 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.Logger;
import me.cortex.voxy.common.util.AllocationArena; import me.cortex.voxy.common.util.AllocationArena;
import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.util.MemoryBuffer;
import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.world.WorldSection; import me.cortex.voxy.common.world.WorldSection;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
@@ -61,6 +63,7 @@ public class AsyncNodeManager {
private final NodeManager manager; private final NodeManager manager;
private final BasicAsyncGeometryManager geometryManager; private final BasicAsyncGeometryManager geometryManager;
private final IGeometryData geometryData; private final IGeometryData geometryData;
private final SectionUpdateRouter router;
private final GeometryCache geometryCache = new GeometryCache(1L<<32); private final GeometryCache geometryCache = new GeometryCache(1L<<32);
@@ -77,7 +80,7 @@ public class AsyncNodeManager {
private boolean needsWaitForSync = false; 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 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 //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 // it MUST ONLY be accessed on the render thread
@@ -99,7 +102,20 @@ public class AsyncNodeManager {
this.thread.setName("Async Node Manager"); this.thread.setName("Async Node Manager");
this.geometryManager = new BasicAsyncGeometryManager(((BasicSectionGeometryData)geometryData).getMaxSectionCount(), this.geometryCapacity); 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 //Dont do the move... is just to much effort
this.manager.setClear(new NodeManager.ICleaner() { this.manager.setClear(new NodeManager.ICleaner() {
@Override @Override
@@ -273,6 +289,12 @@ public class AsyncNodeManager {
continue; continue;
} }
if (pos == 0) {
//THIS SHOULD BE IMPOSSIBLE
//TODO: VVVVV MUCH MEGA FIX
continue;
}
this.manager.removeNodeGeometry(pos); this.manager.removeNodeGeometry(pos);
} }
job.free(); job.free();
@@ -600,7 +622,7 @@ public class AsyncNodeManager {
this.addWork(); this.addWork();
} }
public void submitChildChange(WorldSection section) { private void submitChildChange(WorldSection section) {
if (!this.running) { if (!this.running) {
return; return;
} }
@@ -609,7 +631,7 @@ public class AsyncNodeManager {
this.addWork(); this.addWork();
} }
public void submitGeometryResult(BuiltSection geometry) { private void submitGeometryResult(BuiltSection geometry) {
if (!this.running) { if (!this.running) {
geometry.free(); geometry.free();
return; return;
@@ -724,6 +746,13 @@ public class AsyncNodeManager {
return this.workCounter.get()!=0 && RESULT_HANDLE.get(this) != null; 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 //Results object, which is to be synced between the render thread and worker thread
private static final class SyncResults { private static final class SyncResults {
//Contains //Contains