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 ca57769a..129495f2 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 @@ -1,55 +1,42 @@ package me.cortex.voxy.client.core.rendering; -import io.netty.util.internal.MathUtil; import me.cortex.voxy.client.RenderStatistics; import me.cortex.voxy.client.TimingStatistics; import me.cortex.voxy.client.core.gl.Capabilities; import me.cortex.voxy.client.core.gl.GlTexture; import me.cortex.voxy.client.core.model.ModelBakerySubsystem; -import me.cortex.voxy.client.core.model.ModelStore; -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.hierachical.AsyncNodeManager; import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser; import me.cortex.voxy.client.core.rendering.hierachical.NodeCleaner; -import me.cortex.voxy.client.core.rendering.hierachical.NodeManager; import me.cortex.voxy.client.core.rendering.section.AbstractSectionRenderer; +import me.cortex.voxy.client.core.rendering.section.geometry.*; import me.cortex.voxy.client.core.rendering.section.IUsesMeshlets; import me.cortex.voxy.client.core.rendering.section.MDICSectionRenderer; import me.cortex.voxy.client.core.rendering.util.DownloadStream; -import me.cortex.voxy.client.core.rendering.util.UploadStream; import me.cortex.voxy.common.Logger; -import me.cortex.voxy.common.util.MessageQueue; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.thread.ServiceThreadPool; -import me.cortex.voxy.common.world.WorldSection; -import net.minecraft.client.render.Camera; -import java.lang.invoke.VarHandle; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import static org.lwjgl.opengl.GL42.*; -public class RenderService, J extends Viewport> { +public class RenderService, J extends Viewport, Q extends IGeometryData> { public static final int STATIC_VAO = glGenVertexArrays(); - private static AbstractSectionRenderer createSectionRenderer(ModelStore store, int maxSectionCount, long geometryCapacity) { - return new MDICSectionRenderer(store, maxSectionCount, geometryCapacity); - } - private final ViewportSelector viewportSelector; - private final AbstractSectionRenderer sectionRenderer; + private final Q geometryData; + private final AbstractSectionRenderer sectionRenderer; - private final NodeManager nodeManager; + private final AsyncNodeManager nodeManager; private final NodeCleaner nodeCleaner; private final HierarchicalOcclusionTraverser traversal; private final ModelBakerySubsystem modelService; private final RenderGenerationService renderGen; - private final MessageQueue sectionUpdateQueue; - private final MessageQueue geometryUpdateQueue; - private final WorldEngine world; @SuppressWarnings("unchecked") @@ -60,32 +47,25 @@ public class RenderService, J extends Vi //Max geometry: 1 gb long geometryCapacity = Math.min((1L<<(64-Long.numberOfLeadingZeros(Capabilities.INSTANCE.ssboMaxSize-1)))<<1, 1L<<32)-1024/*(1L<<32)-1024*/; //geometryCapacity = 1<<24; + + this.geometryData = (Q) new BasicSectionGeometryData(1<<20, geometryCapacity); + //Max sections: ~500k - this.sectionRenderer = (T) createSectionRenderer(this.modelService.getStore(),1<<20, geometryCapacity); + this.sectionRenderer = (T) new MDICSectionRenderer(this.modelService.getStore(), (BasicSectionGeometryData) this.geometryData); Logger.info("Using renderer: " + this.sectionRenderer.getClass().getSimpleName()); //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 NodeManager(1<<21, this.sectionRenderer.getGeometryManager(), router); + this.nodeManager = new AsyncNodeManager(1<<21, router, this.geometryData); this.nodeCleaner = new NodeCleaner(this.nodeManager); - this.sectionUpdateQueue = new MessageQueue<>(section -> { - byte childExistence = section.getNonEmptyChildren(); - section.release();//TODO: move this to another thread (probably a service job to free, this is because freeing can cause a DB save which should not happen on the render thread) - this.nodeManager.processChildChange(section.key, childExistence); - }); - this.geometryUpdateQueue = new MessageQueue<>(this.nodeManager::processGeometryResult); - this.viewportSelector = new ViewportSelector<>(this.sectionRenderer::createViewport); this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool, - this.geometryUpdateQueue::push, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets, - ()->this.geometryUpdateQueue.count()<7000); + this.nodeManager::submitGeometryResult, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets, + ()->true); - router.setCallbacks(this.renderGen::enqueueTask, section -> { - section.acquire(); - this.sectionUpdateQueue.push(section); - }); + router.setCallbacks(this.renderGen::enqueueTask, this.nodeManager::submitChildChange); this.traversal = new HierarchicalOcclusionTraverser(this.nodeManager, this.nodeCleaner); @@ -93,14 +73,16 @@ public class RenderService, J extends Vi Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome); world.getMapper().setBiomeCallback(this.modelService::addBiome); + + this.nodeManager.start(); } public void addTopLevelNode(long pos) { - this.nodeManager.insertTopLevelNode(pos); + this.nodeManager.addTopLevel(pos); } public void removeTopLevelNode(long pos) { - this.nodeManager.removeTopLevelNode(pos); + this.nodeManager.removeTopLevel(pos); } public void tickModelService(long budget) { @@ -134,10 +116,7 @@ public class RenderService, J extends Vi TimingStatistics.main.stop(); TimingStatistics.dynamic.start(); - //Tick download stream - //TODO: make this so that can - DownloadStream.INSTANCE.tick(); - + /* this.sectionUpdateQueue.consume(128); //if (this.modelService.getProcessingCount() < 750) @@ -147,13 +126,16 @@ public class RenderService, J extends Vi if (this.nodeManager.writeChanges(this.traversal.getNodeBuffer())) {//TODO: maybe move the node buffer out of the traversal class UploadStream.INSTANCE.commit(); - } + }*/ + + + //Tick download stream + DownloadStream.INSTANCE.tick(); + + this.nodeManager.tick(this.traversal.getNodeBuffer()); + this.nodeCleaner.tick(this.traversal.getNodeBuffer());//Probably do this here?? - - //this needs to go after, due to geometry updates committed by the nodeManager - this.sectionRenderer.getGeometryManager().tick(); - TimingStatistics.dynamic.stop(); TimingStatistics.main.start(); } @@ -177,8 +159,7 @@ public class RenderService, J extends Vi public void addDebugData(List debug) { this.modelService.addDebugData(debug); this.renderGen.addDebugData(debug); - this.sectionRenderer.addDebug(debug); - this.nodeManager.addDebug(debug); + this.sectionRenderer.addDebug(debug); if (RenderStatistics.enabled) { debug.add("HTC: [" + Arrays.stream(flipCopy(RenderStatistics.hierarchicalTraversalCounts)).mapToObj(Integer::toString).collect(Collectors.joining(", "))+"]"); @@ -203,9 +184,6 @@ public class RenderService, J extends Vi this.world.getMapper().setBiomeCallback(null); this.world.getMapper().setStateCallback(null); - //Release all the unprocessed built geometry - this.geometryUpdateQueue.clear(BuiltSection::free); - this.modelService.shutdown(); this.renderGen.shutdown(); this.viewportSelector.free(); @@ -213,9 +191,9 @@ public class RenderService, J extends Vi this.traversal.free(); this.nodeCleaner.free(); - //Release all the unprocessed built geometry - this.geometryUpdateQueue.clear(BuiltSection::free); - this.sectionUpdateQueue.clear(WorldSection::release);//Release anything thats in the queue + this.nodeManager.stop(); + + this.geometryData.free(); } public Viewport getViewport() { 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 94195268..310a11e2 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 @@ -1,7 +1,18 @@ package me.cortex.voxy.client.core.rendering.hierachical; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntConsumer; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongConsumer; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.ISectionWatcher; import me.cortex.voxy.client.core.rendering.building.BuiltSection; +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; +import me.cortex.voxy.client.core.rendering.util.UploadStream; import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.world.WorldSection; import org.lwjgl.system.MemoryUtil; @@ -13,6 +24,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.StampedLock; +import static me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData.SECTION_METADATA_SIZE; + //An "async host" for a NodeManager, has specific synchonius entry and exit points // this is done off thread to reduce the amount of work done on the render thread, improving frame stability and reducing runtime overhead public class AsyncNodeManager { @@ -27,15 +40,29 @@ public class AsyncNodeManager { private final Thread thread; private final StampedLock lock = new StampedLock(); + public final int maxNodeCount; private volatile boolean running = true; private final NodeManager manager; + private final BasicAsyncGeometryManager geometryManager; + private final IGeometryData geometryData; private final AtomicInteger workCounter = new AtomicInteger(); private volatile SyncResults results = null; - public AsyncNodeManager(int maxNodeCount, ISectionWatcher watcher) {//Note the current implmentation of ISectionWatcher is threadsafe + + //locals for during iteration + private final IntOpenHashSet tlnIdChange = new IntOpenHashSet();//"Encoded" add/remove id, first bit indicates if its add or remove, 1 is add + + public AsyncNodeManager(int maxNodeCount, ISectionWatcher watcher, IGeometryData geometryData) { + //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 + // AsyncNodeManager will use an AsyncGeometryManager as the manager for the data store, and sync the results on the render thread + this.geometryData = geometryData; + + this.maxNodeCount = maxNodeCount; this.thread = new Thread(()->{ while (this.running) { this.run(); @@ -43,8 +70,34 @@ public class AsyncNodeManager { //TODO: cleanup here? maybe? }); this.thread.setName("Async Node Manager"); - //TODO: modify BasicSectionGeometryManager to support async updates - this.manager = new NodeManager(maxNodeCount, null, watcher); + + this.geometryManager = new BasicAsyncGeometryManager(((BasicSectionGeometryData)geometryData).getMaxSectionCount(), ((BasicSectionGeometryData)geometryData).getGeometryCapacity()); + this.manager = new NodeManager(maxNodeCount, this.geometryManager, watcher); + this.manager.setClear(new NodeManager.ICleaner() { + @Override + public void alloc(int id) { + + } + + @Override + public void move(int from, int to) { + + } + + @Override + public void free(int id) { + + } + }); + this.manager.setTLNCallbacks(id->{ + if (!this.tlnIdChange.remove(id)) { + this.tlnIdChange.add(id|(1<<31)); + } + }, id -> { + if (!this.tlnIdChange.remove(id|(1<<31))) { + this.tlnIdChange.add(id); + } + }); } private void run() { @@ -59,9 +112,50 @@ public class AsyncNodeManager { return; } - //TODO: limit the number of jobs based on if the amount of updates to be submitted to the render thread gets to large + //This is a funny thing, wait a bit, this allows for better batching, but this thread is independent of everything else so waiting a bit should be mostly ok + try { + Thread.sleep(20); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } int workDone = 0; + + { + LongOpenHashSet add = null; + LongOpenHashSet rem = null; + long stamp = this.tlnLock.writeLock(); + + if (!this.tlnAdd.isEmpty()) { + add = new LongOpenHashSet(this.tlnAdd); + this.tlnAdd.clear(); + } + if (!this.tlnRem.isEmpty()) { + rem = new LongOpenHashSet(this.tlnRem); + this.tlnRem.clear(); + } + + this.tlnLock.unlockWrite(stamp); + int work = 0; + if (rem != null) { + var iter = rem.longIterator(); + while (iter.hasNext()) { + this.manager.removeTopLevelNode(iter.nextLong()); + work++; + } + } + + if (add != null) { + var iter = add.longIterator(); + while (iter.hasNext()) { + this.manager.insertTopLevelNode(iter.nextLong()); + work++; + } + } + + workDone += work; + } + do { var job = this.childUpdateQueue.poll(); if (job == null) @@ -71,32 +165,32 @@ public class AsyncNodeManager { job.release(); } while (true); - do { + for (int limit = 0; limit < 100; limit++) {//Limit uploading var job = this.geometryUpdateQueue.poll(); if (job == null) break; workDone++; this.manager.processGeometryResult(job); - } while (true); + } - do { + for (int limit = 0; limit < 2; limit++) { var job = this.requestBatchQueue.poll(); if (job == null) break; workDone++; long ptr = job.address; - int count = MemoryUtil.memGetInt(ptr); ptr+=4; - if (job.size < count * 8L + 4) { + int count = MemoryUtil.memGetInt(ptr); + ptr += 8;//Its 8 to keep alignment + if (job.size < count * 8L + 8) { throw new IllegalStateException(); } for (int i = 0; i < count; i++) { - long pos = ((long)MemoryUtil.memGetInt(ptr))<<32; ptr += 4; + long pos = ((long) MemoryUtil.memGetInt(ptr)) << 32; ptr += 4; pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4; this.manager.processRequest(pos); } job.free(); - } while (true); - + } do { @@ -105,28 +199,27 @@ public class AsyncNodeManager { break; workDone++; long ptr = job.address; - int count = MemoryUtil.memGetInt(ptr); ptr+=4; - if (job.size < count * 8L + 4) { - throw new IllegalStateException(); - } - for (int i = 0; i < count; i++) { - long pos = ((long)MemoryUtil.memGetInt(ptr))<<32; ptr += 4; + for (int i = 0; i < NodeCleaner.OUTPUT_COUNT; i++) { + long pos = ((long) MemoryUtil.memGetInt(ptr)) << 32; ptr += 4; pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4; + + if (pos == -1) { + //TODO: investigate how or what this happens + continue; + } + this.manager.removeNodeGeometry(pos); } job.free(); } while (true); - - if (this.workCounter.addAndGet(-workDone)<0) { + if (this.workCounter.addAndGet(-workDone) < 0) { throw new IllegalStateException("Work counter less than zero"); } //===================== //process output events and atomically sync to results - - //Events into manager //manager.insertTopLevelNode(); //manager.removeTopLevelNode(); @@ -145,7 +238,6 @@ public class AsyncNodeManager { //manager.writeChanges() - //Run in a loop, process all the input events, collect the output events merge with previous and publish // note: inner event processing is a loop, is.. should be synced to attomic/volatile variable that is being watched // when frametime comes around, want to exit out as quick as possible, or make the event publishing @@ -168,20 +260,189 @@ public class AsyncNodeManager { //TODO: also note! this can be done for the processing of rendered out block models!! // (it might be able to also be put in this thread, maybe? but is proabably worth putting in own thread for latency reasons) + while (RESULT_HANDLE.get(this) != null && this.running) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } - var prev = RESULT_HANDLE.getAndSet(this, null); - //TODO: merge results + + var prev = (SyncResults) RESULT_HANDLE.getAndSet(this, null); SyncResults results = null; + if (prev == null) { + results = new SyncResults(); + //Clear old data (if it exists), create a new result set + results.tlnDelta.addAll(this.tlnIdChange); + this.tlnIdChange.clear(); + + results.geometryUploads.putAll(this.geometryManager.getUploads()); + this.geometryManager.getUploads().clear();//Put in new data into sync set + this.geometryManager.getHeapRemovals().clear();//We dont do removals on new data (as there is "none") + } else { + results = prev; + // merge with the previous result set + + if (!this.tlnIdChange.isEmpty()) {//Merge top level node id changes + var iter = this.tlnIdChange.intIterator(); + while (iter.hasNext()) { + int val = iter.nextInt(); + results.tlnDelta.remove(val ^ (1 << 31));//Remove opposite + results.tlnDelta.add(val);//Add this + } + this.tlnIdChange.clear(); + } + + if (!this.geometryManager.getHeapRemovals().isEmpty()) {//Remove and free all the removed geometry uploads + var rem = this.geometryManager.getHeapRemovals(); + var iter = rem.intIterator(); + while (iter.hasNext()) { + var buffer = results.geometryUploads.remove(iter.nextInt()); + if (buffer != null) { + buffer.free(); + } + } + rem.clear(); + } + + if (!this.geometryManager.getUploads().isEmpty()) {//Add all the new uploads to the result set + var add = this.geometryManager.getUploads(); + var iter = add.int2ObjectEntrySet().fastIterator(); + while (iter.hasNext()) { + var val = iter.next(); + var prevBuffer = results.geometryUploads.put(val.getIntKey(), val.getValue()); + if (prevBuffer != null) { + prevBuffer.free(); + } + } + add.clear(); + } + } + + {//This is the same regardless of if is a merge or new result + //Geometry id metadata updates + if (!this.geometryManager.getUpdateIds().isEmpty()) { + var ids = this.geometryManager.getUpdateIds(); + var iter = ids.intIterator(); + while (iter.hasNext()) { + int val = iter.nextInt(); + int placeId = results.geometryIdUpdateMap.putIfAbsent(val, results.geometryIdUpdateMap.size()); + placeId = placeId==-1?results.geometryIdUpdateMap.size()-1:placeId; + if (512<=placeId) { + throw new IllegalStateException("Outside range of allowed updates"); + } + //Write updated data + this.geometryManager.writeMetadata(val, placeId*32L + results.geometryIdUpdateData.address); + } + ids.clear(); + } + + //Node updates + if (!this.manager.getNodeUpdates().isEmpty()) { + var ids = this.manager.getNodeUpdates(); + var iter = ids.intIterator(); + while (iter.hasNext()) { + int val = iter.nextInt(); + int placeId = results.nodeIdUpdateMap.putIfAbsent(val, results.nodeIdUpdateMap.size()); + placeId = placeId==-1?results.nodeIdUpdateMap.size()-1:placeId; + if (1024<=placeId) { + throw new IllegalStateException("Outside range of allowed updates"); + } + //Write updated data + this.manager.writeNode(val, placeId*16L + results.nodeIdUpdateData.address); + } + ids.clear(); + } + } + + results.geometrySectionCount = this.geometryManager.getSectionCount(); + results.currentMaxNodeId = this.manager.getCurrentMaxNodeId(); + if (!RESULT_HANDLE.compareAndSet(this, null, results)) { throw new IllegalArgumentException("Should always have null"); } - if (prev == null) { - //Clear + } + + private IntConsumer tlnAddCallback; private IntConsumer tlnRemoveCallback; + //Render thread synchronization + public void tick(GlBuffer nodeBuffer) {//TODO: dont pass nodeBuffer here??, do something else thats better + var results = (SyncResults)RESULT_HANDLE.getAndSet(this, null);//Acquire the results + if (results == null) {//There are no new results to process, return + return; } + + //top level node add/remove + if (!results.tlnDelta.isEmpty()) { + var iter = results.tlnDelta.intIterator(); + while (iter.hasNext()) { + int val = iter.nextInt(); + if ((val&(1<<31))!=0) {//Add node + this.tlnAddCallback.accept(val&(-1>>>1)); + } else { + this.tlnRemoveCallback.accept(val); + } + } + //Dont need to clear as is not used again + } + + boolean doCommit = false; + {//Update basic geometry data + var store = (BasicSectionGeometryData)this.geometryData; + store.setSectionCount(results.geometrySectionCount); + + //Do geometry uploads + if (!results.geometryUploads.isEmpty()) { + var iter = results.geometryUploads.int2ObjectEntrySet().fastIterator(); + while (iter.hasNext()) { + var val = iter.next(); + var buffer = val.getValue(); + UploadStream.INSTANCE.upload(store.getGeometryBuffer(), Integer.toUnsignedLong(val.getIntKey()) * 8L, buffer); + buffer.free();//Free the buffer was uploading + } + doCommit = true; + } + + //Do geometry id updates + if (!results.geometryIdUpdateMap.isEmpty()) { + var iter = results.geometryIdUpdateMap.int2IntEntrySet().fastIterator(); + while (iter.hasNext()) { + var val = iter.next(); + long ptr = UploadStream.INSTANCE.upload(store.getMetadataBuffer(), Integer.toUnsignedLong(val.getIntKey()) * SECTION_METADATA_SIZE, SECTION_METADATA_SIZE); + MemoryUtil.memCopy(results.geometryIdUpdateData.address + Integer.toUnsignedLong(val.getIntValue()) * SECTION_METADATA_SIZE, ptr, SECTION_METADATA_SIZE); + } + doCommit = true; + } + } + + //Do node id updates + if (!results.nodeIdUpdateMap.isEmpty()) { + var iter = results.nodeIdUpdateMap.int2IntEntrySet().fastIterator(); + while (iter.hasNext()) { + var val = iter.next(); + long ptr = UploadStream.INSTANCE.upload(nodeBuffer, Integer.toUnsignedLong(val.getIntKey()) * 16L, 16L); + MemoryUtil.memCopy(results.nodeIdUpdateData.address + Integer.toUnsignedLong(val.getIntValue()) * 16L, ptr, 16L); + } + doCommit = true; + } + + + if (doCommit) { + UploadStream.INSTANCE.commit(); + } + results.nodeIdUpdateData.free(); + results.geometryIdUpdateData.free(); + } + + + public void setTLNAddRemoveCallbacks(IntConsumer add, IntConsumer remove) { + this.tlnAddCallback = add; + this.tlnRemoveCallback = remove; } //================================================================================================================== //Incoming events + //TODO: add atomic counters for each event type probably private final ConcurrentLinkedDeque requestBatchQueue = new ConcurrentLinkedDeque<>(); private final ConcurrentLinkedDeque childUpdateQueue = new ConcurrentLinkedDeque<>(); @@ -189,10 +450,15 @@ public class AsyncNodeManager { private final ConcurrentLinkedDeque removeBatchQueue = new ConcurrentLinkedDeque<>(); + private final StampedLock tlnLock = new StampedLock(); + private final LongOpenHashSet tlnAdd = new LongOpenHashSet(); + private final LongOpenHashSet tlnRem = new LongOpenHashSet(); + private void addWork() { if (!this.running) throw new IllegalStateException("Not running"); - this.workCounter.incrementAndGet(); - LockSupport.unpark(this.thread); + if (this.workCounter.getAndIncrement() == 0) { + LockSupport.unpark(this.thread); + } } public void submitRequestBatch(MemoryBuffer batch) { @@ -217,12 +483,31 @@ public class AsyncNodeManager { } public void addTopLevel(long section) { - + if (!this.running) throw new IllegalStateException("Not running"); + long stamp = this.tlnLock.writeLock(); + int state = this.tlnAdd.add(section)?1:0; + state -= this.tlnRem.remove(section)?1:0; + if (state != 0) { + if (this.workCounter.getAndAdd(state) == 0) { + LockSupport.unpark(this.thread); + } + } + this.tlnLock.unlockWrite(stamp); } public void removeTopLevel(long section) { - + if (!this.running) throw new IllegalStateException("Not running"); + long stamp = this.tlnLock.writeLock(); + int state = this.tlnRem.add(section)?1:0; + state -= this.tlnAdd.remove(section)?1:0; + if (state != 0) { + if (this.workCounter.getAndAdd(state) == 0) { + LockSupport.unpark(this.thread); + } + } + this.tlnLock.unlockWrite(stamp); } + //================================================================================================================== public void start() { @@ -245,15 +530,25 @@ public class AsyncNodeManager { } //TODO CLEAN - } - - //Primary synchronization - public void tick() { - var results = RESULT_HANDLE.getAndSet(this, null);//Acquire the results - if (results == null) {//There are no new results to process, return - return; + while (true) { + var buffer = this.requestBatchQueue.poll(); + if (buffer == null) break; + buffer.free(); } + while (true) { + var buffer = this.requestBatchQueue.poll(); + if (buffer == null) break; + buffer.free(); + } + + while (true) { + var buffer = this.geometryUpdateQueue.poll(); + if (buffer == null) break; + buffer.free(); + } + + //TODO: CLEANUP the sync data! } //Results object, which is to be synced between the render thread and worker thread @@ -262,7 +557,27 @@ public class AsyncNodeManager { // geometry uploads and id invalidations and the data // node ids to invalidate/update and its data // top level node ids to add/remove - // cleaner move operations + // cleaner move and set operations + + //Node id updates + size + private final Int2IntOpenHashMap nodeIdUpdateMap = new Int2IntOpenHashMap();//node id to update data location + private final MemoryBuffer nodeIdUpdateData = new MemoryBuffer(8192*2);//capacity for 1024 entries, TODO: ADD RESIZE + private int currentMaxNodeId;// the id of the ending of the node ids + + //TLN add/rem + private final IntOpenHashSet tlnDelta = new IntOpenHashSet(); + + //Deltas for geometry store + private int geometrySectionCount; + private final Int2ObjectOpenHashMap geometryUploads = new Int2ObjectOpenHashMap<>(); + private final Int2IntOpenHashMap geometryIdUpdateMap = new Int2IntOpenHashMap();//geometry id to update data location + private final MemoryBuffer geometryIdUpdateData = new MemoryBuffer(8192*2);//capacity for 512 entries, TODO: ADD RESIZE + + + public SyncResults() { + this.nodeIdUpdateMap.defaultReturnValue(-1); + this.geometryIdUpdateMap.defaultReturnValue(-1); + } } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java index 31932aa0..877af632 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java @@ -12,6 +12,7 @@ import me.cortex.voxy.client.core.rendering.util.HiZBuffer; import me.cortex.voxy.client.core.rendering.Viewport; import me.cortex.voxy.client.core.rendering.util.DownloadStream; import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.world.WorldEngine; import org.lwjgl.system.MemoryUtil; @@ -36,7 +37,7 @@ public class HierarchicalOcclusionTraverser { private static final int MAX_ITERATIONS = WorldEngine.MAX_LOD_LAYER+1; private static final int LOCAL_WORK_SIZE_BITS = 5; - private final NodeManager nodeManager; + private final AsyncNodeManager nodeManager; private final NodeCleaner nodeCleaner; private final GlBuffer requestBuffer; @@ -96,7 +97,7 @@ public class HierarchicalOcclusionTraverser { .compile(); - public HierarchicalOcclusionTraverser(NodeManager nodeManager, NodeCleaner nodeCleaner) { + public HierarchicalOcclusionTraverser(AsyncNodeManager nodeManager, NodeCleaner nodeCleaner) { this.nodeCleaner = nodeCleaner; this.nodeManager = nodeManager; this.requestBuffer = new GlBuffer(REQUEST_QUEUE_SIZE*8L+8).zero(); @@ -119,7 +120,7 @@ public class HierarchicalOcclusionTraverser { .ssboIf("STATISTICS_BUFFER_BINDING", this.statisticsBuffer); this.topNode2idxMapping.defaultReturnValue(-1); - this.nodeManager.setTLNCallbacks(this::addTLN, this::remTLN); + this.nodeManager.setTLNAddRemoveCallbacks(this::addTLN, this::remTLN); } private void addTLN(int id) { @@ -322,19 +323,15 @@ public class HierarchicalOcclusionTraverser { //Logger.warn("Count over max buffer size, clamping, got count: " + count + "."); count = (int) ((this.requestBuffer.size()>>3)-1); + + //Write back the clamped count + MemoryUtil.memPutInt(ptr-8, count); } //if (count > REQUEST_QUEUE_SIZE) { // Logger.warn("Count larger than 'maxRequestCount', overflow captured. Overflowed by " + (count-REQUEST_QUEUE_SIZE)); //} if (count != 0) { - //this.nodeManager.processRequestQueue(count, ptr + 8); - - //It just felt more appropriate putting the loop here - for (int requestIndex = 0; requestIndex < count; requestIndex++) { - long pos = ((long)MemoryUtil.memGetInt(ptr))<<32; ptr += 4; - pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4; - this.nodeManager.processRequest(pos); - } + this.nodeManager.submitRequestBatch(new MemoryBuffer(count*8L+8).cpyFrom(ptr-8));// the -8 is because we incremented it by 8 } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java index c306fda9..e6ca9537 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java @@ -29,7 +29,7 @@ public class NodeCleaner { private static final int SORTING_WORKER_SIZE = 64; private static final int WORK_PER_THREAD = 8; - private static final int OUTPUT_COUNT = 256; + static final int OUTPUT_COUNT = 256; private static final int BATCH_SET_SIZE = 2048; @@ -68,11 +68,11 @@ public class NodeCleaner { private final IntOpenHashSet allocIds = new IntOpenHashSet(); private final IntOpenHashSet freeIds = new IntOpenHashSet(); - private final NodeManager nodeManager; + private final AsyncNodeManager nodeManager; int visibilityId = 0; - public NodeCleaner(NodeManager nodeManager) { + public NodeCleaner(AsyncNodeManager nodeManager) { this.nodeManager = nodeManager; this.visibilityBuffer = new GlBuffer(nodeManager.maxNodeCount*4L).zero(); this.visibilityBuffer.fill(-1); @@ -85,6 +85,7 @@ public class NodeCleaner { .ssbo("VISIBILITY_BUFFER_BINDING", this.visibilityBuffer) .ssbo("OUTPUT_BUFFER_BINDING", this.outputBuffer); + /* this.nodeManager.setClear(new NodeManager.ICleaner() { @Override public void alloc(int id) { @@ -104,6 +105,7 @@ public class NodeCleaner { NodeCleaner.this.allocIds.remove(id); } }); + */ } @@ -114,34 +116,30 @@ public class NodeCleaner { this.setIds(this.freeIds, -1); if (this.shouldCleanGeometry()) { - var gm = this.nodeManager.getGeometryManager(); + this.outputBuffer.fill(this.nodeManager.maxNodeCount - 2);//TODO: maybe dont set to zero?? - int c = (int) (((((double) gm.getUsedCapacity() / gm.geometryCapacity) - 0.75) * 4 * 10) + 1); - c = 1; - for (int i = 0; i < c; i++) { - this.outputBuffer.fill(this.nodeManager.maxNodeCount - 2);//TODO: maybe dont set to zero?? + this.sorter.bind(); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, nodeDataBuffer.id); - this.sorter.bind(); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, nodeDataBuffer.id); + //TODO: choose whether this is in nodeSpace or section/geometryId space + // + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + //glDispatchCompute((this.nodeManager.getCurrentMaxNodeId() + (SORTING_WORKER_SIZE+WORK_PER_THREAD) - 1) / (SORTING_WORKER_SIZE+WORK_PER_THREAD), 1, 1); - //TODO: choose whether this is in nodeSpace or section/geometryId space - // - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - glDispatchCompute((this.nodeManager.getCurrentMaxNodeId() + (SORTING_WORKER_SIZE+WORK_PER_THREAD) - 1) / (SORTING_WORKER_SIZE+WORK_PER_THREAD), 1, 1); + this.resultTransformer.bind(); + glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, this.outputBuffer.id, 0, 4 * OUTPUT_COUNT); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeDataBuffer.id); + glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, this.outputBuffer.id, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.visibilityBuffer.id); + glUniform1ui(0, this.visibilityId); - this.resultTransformer.bind(); - glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, this.outputBuffer.id, 0, 4 * OUTPUT_COUNT); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeDataBuffer.id); - glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, this.outputBuffer.id, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.visibilityBuffer.id); - glUniform1ui(0, this.visibilityId); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + glDispatchCompute(1, 1, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - glDispatchCompute(1, 1, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - - DownloadStream.INSTANCE.download(this.outputBuffer, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT, this::onDownload); - } + DownloadStream.INSTANCE.download(this.outputBuffer, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT, + buffer -> this.nodeManager.submitRemoveBatch(buffer.copy())//Copy into buffer and emit to node manager + ); } } @@ -150,29 +148,7 @@ public class NodeCleaner { //return this.nodeManager.getGeometryManager().getRemainingCapacity() < 1_000_000_000L; //If used more than 75% of geometry buffer - return 3<((double)this.nodeManager.getGeometryManager().getUsedCapacity())/((double)this.nodeManager.getGeometryManager().getRemainingCapacity()); - } - - private void onDownload(long ptr, long size) { - //StringBuilder b = new StringBuilder(); - //Long2IntOpenHashMap aa = new Long2IntOpenHashMap(); - for (int i = 0; i < OUTPUT_COUNT; i++) { - long pos = Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr + 8 * i))<<32; - pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr + 8 * i + 4)); - //aa.addTo(pos, 1); - if (pos == -1) { - //TODO: investigate how or what this happens - continue; - } - //if (WorldEngine.getLevel(pos) == 4 && WorldEngine.getX(pos)<-32) { - // int a = 0; - //} - this.nodeManager.removeNodeGeometry(pos); - //b.append(", ").append(WorldEngine.pprintPos(pos));//.append(((int)((pos>>32)&0xFFFFFFFFL)));// - } - int a = 0; - - //System.out.println(b); + return false;//return 3<((double)this.nodeManager.getGeometryManager().getUsedCapacity())/((double)this.nodeManager.getGeometryManager().getRemainingCapacity()); } private void setIds(IntOpenHashSet collection, int setTo) { diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java index 21fda401..f86cd326 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java @@ -9,7 +9,7 @@ import it.unimi.dsi.fastutil.longs.LongSet; import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.ISectionWatcher; import me.cortex.voxy.client.core.rendering.building.BuiltSection; -import me.cortex.voxy.client.core.rendering.section.AbstractSectionGeometryManager; +import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryManager; import me.cortex.voxy.client.core.rendering.util.UploadStream; import me.cortex.voxy.client.core.util.ExpandingObjectAllocationList; import me.cortex.voxy.common.Logger; @@ -82,7 +82,7 @@ public class NodeManager { private final ExpandingObjectAllocationList singleRequests = new ExpandingObjectAllocationList<>(SingleNodeRequest[]::new); private final ExpandingObjectAllocationList childRequests = new ExpandingObjectAllocationList<>(NodeChildRequest[]::new); private final IntOpenHashSet nodeUpdates = new IntOpenHashSet(); - private final AbstractSectionGeometryManager geometryManager; + private final IGeometryManager geometryManager; private final ISectionWatcher watcher; private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap(); private final NodeStore nodeData; @@ -110,7 +110,7 @@ public class NodeManager { this.topLevelNodeIdRemovedCallback = onRemove; } - public NodeManager(int maxNodeCount, AbstractSectionGeometryManager geometryManager, ISectionWatcher watcher) { + public NodeManager(int maxNodeCount, IGeometryManager geometryManager, ISectionWatcher watcher) { if (!MathUtil.isPowerOfTwo(maxNodeCount)) { throw new IllegalArgumentException("Max node count must be a power of 2"); } @@ -232,6 +232,14 @@ public class NodeManager { } } + private void removeGeometryCached(long pos, int id) { + //Removes geometry possible with downloading to cache + this.geometryManager.removeSection(id); + } + //TODO: FIXME: add method to clear geometry cache of position, or the geometry is empty etc jkdfgsl + // this is for cpu/ram side geometry caching + // TODO: IMPLEMENT + private int uploadReplaceSection(int meshId, BuiltSection section) { if (section.isEmpty()) { if (meshId != NULL_GEOMETRY_ID && meshId != EMPTY_GEOMETRY_ID) { @@ -317,13 +325,14 @@ public class NodeManager { byte rem = (byte) (change&oldMsk); for (int i = 0; i < 8; i++) { if ((rem&(1<{ - //TODO: download and remove instead of just removing, and store in ram cache for later!! - section.free(); - }); + this.removeGeometryCached(pos, meshId); this.nodeData.setNodeGeometry(nodeId, NULL_GEOMETRY_ID); this.invalidateNode(nodeId);//Only need to invalidate on change this.nodeData.unmarkNodeGeometryInFlight(nodeId);//Remove geometry inflight as well, its removed } else { - if (geometryId == NULL_GEOMETRY_ID) { + if (meshId == NULL_GEOMETRY_ID) { //Logger.info("Tried removing geometry of internal node but geometry was null"); } } @@ -1330,6 +1339,16 @@ public class NodeManager { return true; } + //Used for raw access to the update map, internal (used in async) + IntOpenHashSet getNodeUpdates() { + return this.nodeUpdates; + } + + //Used to write a specified node into a specific address (used in async) + void writeNode(int node, long address) { + this.nodeData.writeNode(address, node); + } + public MemoryBuffer _generateChangeList() { //For internal testing use only if (this.nodeUpdates.isEmpty()) { @@ -1388,9 +1407,6 @@ public class NodeManager { return this.nodeData.getEndNodeId(); } - public AbstractSectionGeometryManager getGeometryManager() { - return this.geometryManager; - } //================================================================================================================== diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/TestNodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/TestNodeManager.java index 2bfcb9f5..aa2219c4 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/TestNodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/TestNodeManager.java @@ -5,7 +5,7 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.longs.*; import me.cortex.voxy.client.core.rendering.ISectionWatcher; import me.cortex.voxy.client.core.rendering.building.BuiltSection; -import me.cortex.voxy.client.core.rendering.section.AbstractSectionGeometryManager; +import me.cortex.voxy.client.core.rendering.section.geometry.AbstractSectionGeometryManager; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.HierarchicalBitSet; import me.cortex.voxy.common.util.MemoryBuffer; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/AbstractSectionRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/AbstractSectionRenderer.java index 12498a1a..95e811fb 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/AbstractSectionRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/AbstractSectionRenderer.java @@ -1,15 +1,15 @@ package me.cortex.voxy.client.core.rendering.section; -import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.gl.GlTexture; import me.cortex.voxy.client.core.model.ModelStore; import me.cortex.voxy.client.core.rendering.Viewport; +import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData; import java.util.List; //Takes in mesh ids from the hierachical traversal and may perform more culling then renders it -public abstract class AbstractSectionRenderer , J extends AbstractSectionGeometryManager> { +public abstract class AbstractSectionRenderer , J extends IGeometryData> { protected final J geometryManager; protected final ModelStore modelStore; protected AbstractSectionRenderer(ModelStore modelStore, J geometryManager) { @@ -22,9 +22,7 @@ public abstract class AbstractSectionRenderer , J extends public abstract void renderTemporal(GlTexture depthBoundTexture); public abstract void renderTranslucent(T viewport, GlTexture depthBoundTexture); public abstract T createViewport(); - public void free() { - this.geometryManager.free(); - } + public abstract void free(); public J getGeometryManager() { return this.geometryManager; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/MDICSectionRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/MDICSectionRenderer.java index e0a17e19..8459dceb 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/MDICSectionRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/MDICSectionRenderer.java @@ -7,6 +7,7 @@ import me.cortex.voxy.client.core.gl.GlTexture; import me.cortex.voxy.client.core.gl.shader.Shader; import me.cortex.voxy.client.core.gl.shader.ShaderType; import me.cortex.voxy.client.core.model.ModelStore; +import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; import me.cortex.voxy.client.core.rendering.util.LightMapHelper; import me.cortex.voxy.client.core.rendering.RenderService; import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; @@ -31,10 +32,9 @@ import static org.lwjgl.opengl.GL33.glBindSampler; import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER; import static org.lwjgl.opengl.GL43.*; import static org.lwjgl.opengl.GL45.glBindTextureUnit; -import static org.lwjgl.opengl.GL45.glCopyNamedBufferSubData; //Uses MDIC to render the sections -public class MDICSectionRenderer extends AbstractSectionRenderer { +public class MDICSectionRenderer extends AbstractSectionRenderer { private static final int TRANSLUCENT_OFFSET = 400_000;//in draw calls private static final int TEMPORAL_OFFSET = 500_000;//in draw calls private static final int STATISTICS_BUFFER_BINDING = 7; @@ -73,14 +73,10 @@ public class MDICSectionRenderer extends AbstractSectionRenderer lines) { super.addDebug(lines); - lines.add("SC/GS: " + this.geometryManager.getSectionCount() + "/" + (this.geometryManager.getGeometryUsed()/(1024*1024)));//section count/geometry size (MB) + //lines.add("SC/GS: " + this.geometryManager.getSectionCount() + "/" + (this.geometryManager.getGeometryUsed()/(1024*1024)));//section count/geometry size (MB) } @Override public MDICViewport createViewport() { - return new MDICViewport(this.maxSectionCount); + return new MDICViewport(this.geometryManager.getMaxSectionCount()); } @Override public void free() { - super.free(); this.uniform.free(); this.terrainShader.free(); this.commandGenShader.free(); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/AbstractSectionGeometryManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/AbstractSectionGeometryManager.java similarity index 91% rename from src/main/java/me/cortex/voxy/client/core/rendering/section/AbstractSectionGeometryManager.java rename to src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/AbstractSectionGeometryManager.java index 2eb70c37..cd8c75b2 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/AbstractSectionGeometryManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/AbstractSectionGeometryManager.java @@ -1,4 +1,4 @@ -package me.cortex.voxy.client.core.rendering.section; +package me.cortex.voxy.client.core.rendering.section.geometry; import me.cortex.voxy.client.core.rendering.building.BuiltSection; import net.caffeinemc.mods.sodium.client.util.MathUtil; @@ -8,7 +8,7 @@ import java.util.function.Consumer; //Does not care about the position of the sections, multiple sections that have the same position can be uploaded // it is up to the traversal system to manage what sections exist in the geometry buffer // the system is basicly "dumb" as in it just follows orders -public abstract class AbstractSectionGeometryManager { +public abstract class AbstractSectionGeometryManager implements IGeometryManager { public final int maxSections; public final long geometryCapacity; protected AbstractSectionGeometryManager(int maxSections, long geometryCapacity) { diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicAsyncGeometryManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicAsyncGeometryManager.java new file mode 100644 index 00000000..a5877700 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicAsyncGeometryManager.java @@ -0,0 +1,157 @@ +package me.cortex.voxy.client.core.rendering.section.geometry; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import me.cortex.voxy.client.core.rendering.building.BuiltSection; +import me.cortex.voxy.common.util.AllocationArena; +import me.cortex.voxy.common.util.HierarchicalBitSet; +import me.cortex.voxy.common.util.MemoryBuffer; +import org.lwjgl.system.MemoryUtil; + +import java.util.function.Consumer; + +import static me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryManager.SECTION_METADATA_SIZE; + +//Is basicly the manager for an "undefined" data store, the underlying store is irrelevant +// this manager serves as an overlay, that is, it allows an implementation to do "async management" of the data store +public class BasicAsyncGeometryManager implements IGeometryManager { + private static final int GEOMETRY_ELEMENT_SIZE = 8; + private final HierarchicalBitSet allocationSet; + private final AllocationArena allocationHeap = new AllocationArena(); + private final ObjectArrayList sectionMetadata = new ObjectArrayList<>(1<<15); + + //Changes that need to be applied to the underlying data store to match this state + private final IntOpenHashSet invalidatedIds = new IntOpenHashSet(1024);//Ids that need to be invalidated + //TODO: maybe change from it pointing to MemoryBuffer, to BuiltSection + //Note!: the int part is an unsigned int ptr, must be scaled by GEOMETRY_ELEMENT_SIZE + private final Int2ObjectOpenHashMap heapUploads = new Int2ObjectOpenHashMap<>(1024);//Uploads into the buffer at the given location + private final IntOpenHashSet heapRemoveUploads = new IntOpenHashSet(1024);//Any removals are added here, so that it can be properly synced + + public BasicAsyncGeometryManager(int maxSectionCount, long geometryCapacity) { + this.allocationSet = new HierarchicalBitSet(maxSectionCount); + if (geometryCapacity%GEOMETRY_ELEMENT_SIZE != 0) throw new IllegalStateException(); + this.allocationHeap.setLimit(geometryCapacity/GEOMETRY_ELEMENT_SIZE); + } + + @Override + public int uploadSection(BuiltSection section) { + return this.uploadReplaceSection(-1, section); + } + + @Override + public int uploadReplaceSection(int oldId, BuiltSection section) { + if (section.isEmpty()) { + throw new IllegalArgumentException("sectionData is empty, cannot upload nothing"); + } + + //Free the old id and replace it with a new one + // if oldId is -1, then treat it as not previously existing + + //Free the old data if oldId is supplied + if (oldId != -1) { + //Its here just for future optimization potential + this.removeSection(oldId); + } + + int newId = this.allocationSet.allocateNext(); + if (newId == HierarchicalBitSet.SET_FULL) { + throw new IllegalStateException("Tried adding section when section count is already at capacity"); + } + if (newId > this.sectionMetadata.size()) { + throw new IllegalStateException("Size exceeds limits: " + newId + ", " + this.sectionMetadata.size() + ", " + this.allocationSet.getCount()); + } + + var newMeta = this.createMeta(section); + + if (newId == this.sectionMetadata.size()) { + this.sectionMetadata.add(newMeta); + } else { + this.sectionMetadata.set(newId, newMeta); + } + + //Invalidate the section id + this.invalidatedIds.add(newId); + + //HierarchicalOcclusionTraverser.HACKY_SECTION_COUNT = this.allocationSet.getCount(); + return newId; + } + + @Override + public void removeSection(int id) { + if (!this.allocationSet.free(id)) { + throw new IllegalStateException("Id was not already allocated. id: " + id); + } + var oldMetadata = this.sectionMetadata.set(id, null); + int ptr = oldMetadata.geometryPtr; + //Free from the heap + this.allocationHeap.free(Integer.toUnsignedLong(ptr)); + //Free the upload if it was uploading + var buf = this.heapUploads.remove(ptr); + if (buf != null) { + buf.free(); + } + this.heapRemoveUploads.add(ptr); + this.invalidatedIds.add(id); + } + + private SectionMeta createMeta(BuiltSection section) { + if ((section.geometryBuffer.size%GEOMETRY_ELEMENT_SIZE)!=0) throw new IllegalStateException(); + int size = (int) (section.geometryBuffer.size/GEOMETRY_ELEMENT_SIZE); + //Address + int addr = (int)this.allocationHeap.alloc(size); + //Create upload + if (this.heapUploads.put(addr, section.geometryBuffer) != null) { + throw new IllegalStateException(); + } + this.heapRemoveUploads.remove(addr); + //Create Meta + return new SectionMeta(section.position, section.aabb, addr, size, section.offsets, section.childExistence); + } + + @Override + public void downloadAndRemove(int id, Consumer callback) { + throw new IllegalStateException("Not yet implemented"); + } + + public Int2ObjectOpenHashMap getUploads() { + return this.heapUploads; + } + + public IntOpenHashSet getHeapRemovals() { + return this.heapRemoveUploads; + } + + public int getSectionCount() { + return this.allocationSet.getCount(); + } + + public IntOpenHashSet getUpdateIds() { + return this.invalidatedIds; + } + + public void writeMetadata(int sectionId, long ptr) { + var sec = this.sectionMetadata.get(sectionId); + if (sec == null) { + //Write nothing + MemoryUtil.memSet(ptr, 0, SECTION_METADATA_SIZE); + } else { + sec.writeMetadata(ptr); + } + } + + private record SectionMeta(long position, int aabb, int geometryPtr, int itemCount, int[] offsets, byte childExistence) { + public void writeMetadata(long ptr) { + //Split the long into 2 ints to solve endian issues + MemoryUtil.memPutInt(ptr, (int) (this.position>>32)); ptr += 4; + MemoryUtil.memPutInt(ptr, (int) this.position); ptr += 4; + MemoryUtil.memPutInt(ptr, (int) this.aabb); ptr += 4; + MemoryUtil.memPutInt(ptr, this.geometryPtr + this.offsets[0]); ptr += 4; + + MemoryUtil.memPutInt(ptr, (this.offsets[1]-this.offsets[0])|((this.offsets[2]-this.offsets[1])<<16)); ptr += 4; + MemoryUtil.memPutInt(ptr, (this.offsets[3]-this.offsets[2])|((this.offsets[4]-this.offsets[3])<<16)); ptr += 4; + MemoryUtil.memPutInt(ptr, (this.offsets[5]-this.offsets[4])|((this.offsets[6]-this.offsets[5])<<16)); ptr += 4; + MemoryUtil.memPutInt(ptr, (this.offsets[7]-this.offsets[6])|((this.itemCount -this.offsets[7])<<16)); ptr += 4; + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryData.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryData.java new file mode 100644 index 00000000..db8ab888 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryData.java @@ -0,0 +1,52 @@ +package me.cortex.voxy.client.core.rendering.section.geometry; + +import me.cortex.voxy.client.core.gl.GlBuffer; + +public class BasicSectionGeometryData implements IGeometryData { + public static final int SECTION_METADATA_SIZE = 32; + private final GlBuffer sectionMetadataBuffer; + private final GlBuffer geometryBuffer; + + private final int maxSectionCount; + private int currentSectionCount; + + public BasicSectionGeometryData(int maxSectionCount, long geometryCapacity) { + this.maxSectionCount = maxSectionCount; + this.sectionMetadataBuffer = new GlBuffer((long) maxSectionCount * SECTION_METADATA_SIZE); + //8 Cause a quad is 8 bytes + if ((geometryCapacity%8)!=0) { + throw new IllegalStateException(); + } + this.geometryBuffer = new GlBuffer(geometryCapacity); + } + + public GlBuffer getGeometryBuffer() { + return this.geometryBuffer; + } + + public GlBuffer getMetadataBuffer() { + return this.sectionMetadataBuffer; + } + + public int getSectionCount() { + return this.currentSectionCount; + } + + public void setSectionCount(int count) { + this.currentSectionCount = count; + } + + public int getMaxSectionCount() { + return this.maxSectionCount; + } + + public long getGeometryCapacity() {//In bytes + return this.geometryBuffer.size(); + } + + @Override + public void free() { + this.sectionMetadataBuffer.free(); + this.geometryBuffer.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/BasicSectionGeometryManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryManager.java similarity index 98% rename from src/main/java/me/cortex/voxy/client/core/rendering/section/BasicSectionGeometryManager.java rename to src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryManager.java index b12ca3f1..f61621a1 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/BasicSectionGeometryManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryManager.java @@ -1,4 +1,4 @@ -package me.cortex.voxy.client.core.rendering.section; +package me.cortex.voxy.client.core.rendering.section.geometry; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectArrayList; @@ -12,7 +12,7 @@ import org.lwjgl.system.MemoryUtil; import java.util.function.Consumer; public class BasicSectionGeometryManager extends AbstractSectionGeometryManager { - private static final int SECTION_METADATA_SIZE = 32; + public static final int SECTION_METADATA_SIZE = 32; private final GlBuffer sectionMetadataBuffer; private final BufferArena geometry; private final HierarchicalBitSet allocationSet; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IGeometryData.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IGeometryData.java new file mode 100644 index 00000000..818c43c7 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IGeometryData.java @@ -0,0 +1,5 @@ +package me.cortex.voxy.client.core.rendering.section.geometry; + +public interface IGeometryData { + void free(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IGeometryManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IGeometryManager.java new file mode 100644 index 00000000..e73f206d --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IGeometryManager.java @@ -0,0 +1,13 @@ +package me.cortex.voxy.client.core.rendering.section.geometry; + +import me.cortex.voxy.client.core.rendering.building.BuiltSection; + +import java.util.function.Consumer; + +public interface IGeometryManager { + int uploadSection(BuiltSection section); + int uploadReplaceSection(int oldId, BuiltSection section); + void removeSection(int id); + + void downloadAndRemove(int id, Consumer callback); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java index b7a64959..88a1a71a 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java @@ -6,6 +6,7 @@ import me.cortex.voxy.client.core.gl.GlFence; import me.cortex.voxy.client.core.gl.GlPersistentMappedBuffer; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.AllocationArena; +import me.cortex.voxy.common.util.MemoryBuffer; import java.util.ArrayDeque; import java.util.Deque; @@ -36,6 +37,9 @@ public class UploadStream { private long caddr = -1; private long offset = 0; + public void upload(GlBuffer buffer, long destOffset, MemoryBuffer data) {//Note: does not free data, nor does it commit + data.cpyTo(this.upload(buffer, destOffset, data.size)); + } public long upload(GlBuffer buffer, long destOffset, long size) { if (destOffset<0) { throw new IllegalArgumentException(); @@ -81,6 +85,9 @@ public class UploadStream { public void commit() { + if (this.uploadList.isEmpty()) { + return; + } glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT|GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT|GL_BUFFER_UPDATE_BARRIER_BIT); //Execute all the copies for (var entry : this.uploadList) { diff --git a/src/main/java/me/cortex/voxy/common/util/AllocationArena.java b/src/main/java/me/cortex/voxy/common/util/AllocationArena.java index b439c327..59f101e1 100644 --- a/src/main/java/me/cortex/voxy/common/util/AllocationArena.java +++ b/src/main/java/me/cortex/voxy/common/util/AllocationArena.java @@ -1,7 +1,10 @@ package me.cortex.voxy.common.util; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; import it.unimi.dsi.fastutil.longs.LongRBTreeSet; +import java.util.Random; + //FIXME: NOTE: if there is a free block of size > 2^30 EVERYTHING BREAKS, need to either increase size // or automatically split and manage multiple blocks which is very painful //OR instead of addr, defer to a long[] and use indicies @@ -11,20 +14,27 @@ import it.unimi.dsi.fastutil.longs.LongRBTreeSet; public class AllocationArena { public static final long SIZE_LIMIT = -1; - private final int ADDR_BITS = 34;//This gives max size per allocation of 2^30 and max address of 2^39 - private final int SIZE_BITS = 64 - ADDR_BITS; - private final long SIZE_MSK = (1L<sizeLimit) { + this.resized = true; + long addr = this.totalSize; + if (this.totalSize+size>this.sizeLimit) { return SIZE_LIMIT; } - totalSize += size; - TAKEN.add((addr<>> ADDR_BITS) == size) {//If the allocation and slot is the same size, just add it to the taken - TAKEN.add((slot<>> ADDR_BITS)); + this.TAKEN.add((slot<>> ADDR_BITS)); } else { - TAKEN.add(((slot&ADDR_MSK)<>> ADDR_BITS)-size)<>> ADDR_BITS)-size)<>SIZE_BITS != addr) { throw new IllegalStateException(); @@ -76,15 +86,15 @@ public class AllocationArena { long endAddr = (prevSlot>>>SIZE_BITS) + (prevSlot&SIZE_MSK); if (endAddr != addr) {//It means there is a free slot that needs to get merged into long delta = (addr - endAddr); - FREE.remove((delta<>>SIZE_BITS) + (slot&SIZE_MSK); if (endAddr != nextSlot>>>SIZE_BITS) {//It means there is a memory block to be merged in FREE long delta = ((nextSlot>>>SIZE_BITS) - endAddr); - FREE.remove((delta<>>SIZE_BITS) | (slot<>>SIZE_BITS)+(slot&SIZE_MSK); long delta = (next>>>SIZE_BITS) - endAddr; if (extra <= delta) { - FREE.remove((delta<sizeLimit)//If expanding and we would exceed the size limit, dont resize + if (this.totalSize+extra>this.sizeLimit)//If expanding and we would exceed the size limit, dont resize return false; iter.remove(); - TAKEN.add(updatedSlot); - totalSize += extra; - resized = true; + this.TAKEN.add(updatedSlot); + this.totalSize += extra; + //this.resized = true; return true; } } public long getSize(long addr) { addr &= ADDR_MSK; - var iter = TAKEN.iterator(addr << SIZE_BITS); + var iter = this.TAKEN.iterator(addr << SIZE_BITS); if (!iter.hasNext()) throw new IllegalArgumentException(); long slot = iter.nextLong(); @@ -171,5 +181,8 @@ public class AllocationArena { public void setLimit(long size) { this.sizeLimit = size; + if (this.sizeLimit < this.totalSize) { + throw new IllegalStateException("Size set smaller than current size"); + } } }