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() {
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);
{

View File

@@ -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<BuiltSection> 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();
}
}
}

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");
//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);

View File

@@ -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;
}

View File

@@ -57,21 +57,20 @@ public class RenderGenerationService {
private final WorldEngine world;
private final ModelBakerySubsystem modelBakery;
private final Consumer<BuiltSection> resultConsumer;
private Consumer<BuiltSection> resultConsumer;
private final boolean emitMeshlets;
private final ServiceSlice threads;
public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer<BuiltSection> 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<BuiltSection> 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<BuiltSection> 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) {
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
if (this.resultConsumer != null) {
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.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