From 1acb063b834dbb20b2b440964c762c53bc89d847 Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Fri, 9 May 2025 21:36:17 +1000 Subject: [PATCH] start --- .../hierachical/AsyncNodeManager.java | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java 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 new file mode 100644 index 00000000..94195268 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java @@ -0,0 +1,268 @@ +package me.cortex.voxy.client.core.rendering.hierachical; + +import me.cortex.voxy.client.core.rendering.ISectionWatcher; +import me.cortex.voxy.client.core.rendering.building.BuiltSection; +import me.cortex.voxy.common.util.MemoryBuffer; +import me.cortex.voxy.common.world.WorldSection; +import org.lwjgl.system.MemoryUtil; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.StampedLock; + +//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 { + private static final VarHandle RESULT_HANDLE; + static { + try { + RESULT_HANDLE = MethodHandles.lookup().findVarHandle(AsyncNodeManager.class, "results", SyncResults.class); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private final Thread thread; + private final StampedLock lock = new StampedLock(); + private volatile boolean running = true; + + private final NodeManager manager; + + 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 + this.thread = new Thread(()->{ + while (this.running) { + this.run(); + } + //TODO: cleanup here? maybe? + }); + this.thread.setName("Async Node Manager"); + //TODO: modify BasicSectionGeometryManager to support async updates + this.manager = new NodeManager(maxNodeCount, null, watcher); + } + + private void run() { + if (this.workCounter.get() == 0) { + LockSupport.park(); + if (this.workCounter.get() == 0) {//No work + return; + } + } + + if (!this.running) { + return; + } + + //TODO: limit the number of jobs based on if the amount of updates to be submitted to the render thread gets to large + + int workDone = 0; + do { + var job = this.childUpdateQueue.poll(); + if (job == null) + break; + workDone++; + this.manager.processChildChange(job.key, job.getNonEmptyChildren()); + job.release(); + } while (true); + + do { + var job = this.geometryUpdateQueue.poll(); + if (job == null) + break; + workDone++; + this.manager.processGeometryResult(job); + } while (true); + + do { + 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) { + throw new IllegalStateException(); + } + for (int i = 0; i < count; i++) { + 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 { + var job = this.removeBatchQueue.poll(); + if (job == null) + 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; + pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4; + this.manager.removeNodeGeometry(pos); + } + job.free(); + } while (true); + + + 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(); + + //manager.removeNodeGeometry(); + + //manager.processRequest(); + //manager.processChildChange(); + //manager.processGeometryResult(); + + + //Outputs from manager + //manager.setClear(); + //manager.setTLNCallbacks(); + + //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 + // "effectivly immediately", that is, atomicly swap out the render side event updates + + //like + // var current = + // var old = getAndSet(this.events, null); + // if (old != null) {current = merge(old, current);} + // getAndSet(this.events, current); + // if (old == null) {cleanAllEventsUpToThisPoint();}//(i.e. clear any buffers or maps containing data revolving around uncommited render thread data events) + + // this creates a lock free event update loop, allowing the render thread to never stall on waiting + + //TODO: NOTE: THIS MUST BE A SINGLE OBJECT THAT IS EXCHANGED + // for it to be effectivly synchonized all outgoing events/effects _MUST_ happen at the same time + // for this to be lock free an entire object containing ALL the events that must be synced must be exchanged + + + //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) + + + var prev = RESULT_HANDLE.getAndSet(this, null); + //TODO: merge results + SyncResults results = null; + if (!RESULT_HANDLE.compareAndSet(this, null, results)) { + throw new IllegalArgumentException("Should always have null"); + } + if (prev == null) { + //Clear + } + } + + //================================================================================================================== + //Incoming events + //TODO: add atomic counters for each event type probably + private final ConcurrentLinkedDeque requestBatchQueue = new ConcurrentLinkedDeque<>(); + private final ConcurrentLinkedDeque childUpdateQueue = new ConcurrentLinkedDeque<>(); + private final ConcurrentLinkedDeque geometryUpdateQueue = new ConcurrentLinkedDeque<>(); + + private final ConcurrentLinkedDeque removeBatchQueue = new ConcurrentLinkedDeque<>(); + + private void addWork() { + if (!this.running) throw new IllegalStateException("Not running"); + this.workCounter.incrementAndGet(); + LockSupport.unpark(this.thread); + } + + public void submitRequestBatch(MemoryBuffer batch) { + this.requestBatchQueue.add(batch); + this.addWork(); + } + + public void submitChildChange(WorldSection section) { + section.acquire();//We must acquire the section before putting in the queue + this.childUpdateQueue.add(section); + this.addWork(); + } + + public void submitGeometryResult(BuiltSection geometry) { + this.geometryUpdateQueue.add(geometry); + this.addWork(); + } + + public void submitRemoveBatch(MemoryBuffer batch) { + this.removeBatchQueue.add(batch); + this.addWork(); + } + + public void addTopLevel(long section) { + + } + + public void removeTopLevel(long section) { + + } + //================================================================================================================== + + public void start() { + this.thread.start(); + } + + public void stop() { + if (!this.running) { + throw new IllegalStateException(); + } + this.running = false; + LockSupport.unpark(this.thread); + try { + while (this.thread.isAlive()) { + LockSupport.unpark(this.thread); + this.thread.join(1000); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + //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; + } + + } + + //Results object, which is to be synced between the render thread and worker thread + private static final class SyncResults { + //Contains + // 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 + + } +}