inital implementation of async manager

This commit is contained in:
mcrcortex
2025-05-12 19:48:17 +10:00
parent 1acb063b83
commit d1957680e8
16 changed files with 763 additions and 241 deletions

View File

@@ -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<T extends AbstractSectionRenderer<J, ?>, J extends Viewport<J>> {
public class RenderService<T extends AbstractSectionRenderer<J, Q>, J extends Viewport<J>, 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<J, ?> sectionRenderer;
private final Q geometryData;
private final AbstractSectionRenderer<J, Q> 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<WorldSection> sectionUpdateQueue;
private final MessageQueue<BuiltSection> geometryUpdateQueue;
private final WorldEngine world;
@SuppressWarnings("unchecked")
@@ -60,32 +47,25 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, 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<T extends AbstractSectionRenderer<J, ?>, 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<T extends AbstractSectionRenderer<J, ?>, 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<T extends AbstractSectionRenderer<J, ?>, 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();
}
@@ -178,7 +160,6 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
this.modelService.addDebugData(debug);
this.renderGen.addDebugData(debug);
this.sectionRenderer.addDebug(debug);
this.nodeManager.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<T extends AbstractSectionRenderer<J, ?>, 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<T extends AbstractSectionRenderer<J, ?>, 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() {

View File

@@ -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<MemoryBuffer> requestBatchQueue = new ConcurrentLinkedDeque<>();
private final ConcurrentLinkedDeque<WorldSection> childUpdateQueue = new ConcurrentLinkedDeque<>();
@@ -189,10 +450,15 @@ public class AsyncNodeManager {
private final ConcurrentLinkedDeque<MemoryBuffer> 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<MemoryBuffer> 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);
}
}
}

View File

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

View File

@@ -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) {

View File

@@ -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<SingleNodeRequest> singleRequests = new ExpandingObjectAllocationList<>(SingleNodeRequest[]::new);
private final ExpandingObjectAllocationList<NodeChildRequest> 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<<i))==0) continue;
//Remove child geometry and from being watched and activeSections
long cPos = makeChildPos(pos, i);
int meshId = request.removeAndUnRequire(i);
if (meshId != NULL_GEOMETRY_ID && meshId != EMPTY_GEOMETRY_ID) {
this.geometryManager.removeSection(meshId);
this.removeGeometryCached(cPos, meshId);
}
//Remove child from being watched and activeSections
long cPos = makeChildPos(pos, i);
if (this.activeSectionMap.remove(cPos) == -1) {//TODO: verify the removed section is a request type of child and the request id matches this
throw new IllegalStateException("Child pos was in a request but not in active section map");
}
@@ -429,13 +438,15 @@ public class NodeManager {
//There are things in the request to remove
for (int i = 0; i < 8; i++) {
if ((reqRem & (1 << i)) == 0) continue;
//Remove child geometry and from being watched and activeSections
long cPos = makeChildPos(pos, i);
int meshId = request.removeAndUnRequire(i);
if (meshId != NULL_GEOMETRY_ID && meshId != EMPTY_GEOMETRY_ID) {
this.geometryManager.removeSection(meshId);
this.removeGeometryCached(cPos, meshId);
}
//Remove child from being watched and activeSections
long cPos = makeChildPos(pos, i);
int cnid = this.activeSectionMap.remove(cPos);
if (cnid == -1 || (cnid&NODE_TYPE_MSK) != NODE_TYPE_REQUEST) {//TODO: verify the removed section is a request type of child and the request id matches this
throw new IllegalStateException("Child pos was in a request but not in active section map");
@@ -621,12 +632,13 @@ public class NodeManager {
for (int i = 0; i < 8; i++) {
if ((req.getMsk()&(1<<i))==0) continue;
int mesh = req.getChildMesh(i);
if (mesh != EMPTY_GEOMETRY_ID && mesh != NULL_GEOMETRY_ID)
this.geometryManager.removeSection(mesh);
//Unwatch the request position
long childPos = makeChildPos(pos, i);
int meshId = req.getChildMesh(i);
if (meshId != EMPTY_GEOMETRY_ID && meshId != NULL_GEOMETRY_ID)
this.removeGeometryCached(childPos, meshId);
//Remove from section tracker
int cId = this.activeSectionMap.remove(childPos);
if (cId == -1) {
@@ -748,9 +760,9 @@ public class NodeManager {
if (!onlyRemoveChildren) {
//Free geometry and related memory for this node
int geometry = this.nodeData.getNodeGeometry(nodeId);
if (geometry != EMPTY_GEOMETRY_ID && geometry != NULL_GEOMETRY_ID)
this.geometryManager.removeSection(geometry);
int meshId = this.nodeData.getNodeGeometry(nodeId);
if (meshId != EMPTY_GEOMETRY_ID && meshId != NULL_GEOMETRY_ID)
this.removeGeometryCached(pos, meshId);
this.nodeData.free(nodeId);
this.clearFreeId(nodeId);
@@ -779,9 +791,9 @@ public class NodeManager {
this.singleRequests.release(nodeId);
if (req.hasMeshSet()) {
int mesh = req.getMesh();
if (mesh != EMPTY_GEOMETRY_ID && mesh != NULL_GEOMETRY_ID)
this.geometryManager.removeSection(mesh);
int meshId = req.getMesh();
if (meshId != EMPTY_GEOMETRY_ID && meshId != NULL_GEOMETRY_ID)
this.removeGeometryCached(pos, meshId);
}
} else {
@@ -1293,26 +1305,23 @@ public class NodeManager {
}
private void clearGeometryInternal(long pos, int nodeId) {
int geometryId = this.nodeData.getNodeGeometry(nodeId);
int meshId = this.nodeData.getNodeGeometry(nodeId);
//TODO: if isNodeGeometryInFlight is true and geometryId == NULL_GEOMETRY_ID, probably need to
// unwatch from watcher and unmark
if (geometryId != NULL_GEOMETRY_ID && geometryId != EMPTY_GEOMETRY_ID) {
if (meshId != NULL_GEOMETRY_ID && meshId != EMPTY_GEOMETRY_ID) {
//Unwatch node geometry changes
if (this.watcher.unwatch(pos, WorldEngine.UPDATE_TYPE_BLOCK_BIT)) {
throw new IllegalStateException("Unwatching position for geometry removal at: " + WorldEngine.pprintPos(pos) + " resulted in full removal");
}
//Remove geometry and set to null
this.geometryManager.downloadAndRemove(geometryId, section->{
//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;
}
//==================================================================================================================

View File

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

View File

@@ -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 <T extends Viewport<T>, J extends AbstractSectionGeometryManager> {
public abstract class AbstractSectionRenderer <T extends Viewport<T>, 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 <T extends Viewport<T>, 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;

View File

@@ -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<MDICViewport, BasicSectionGeometryManager> {
public class MDICSectionRenderer extends AbstractSectionRenderer<MDICViewport, BasicSectionGeometryData> {
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<MDICViewport, B
//Statistics
private final GlBuffer statisticsBuffer = new GlBuffer(1024).zero();
private final int maxSectionCount;
public MDICSectionRenderer(ModelStore modelStore, int maxSectionCount, long geometryCapacity) {
super(modelStore, new BasicSectionGeometryManager(maxSectionCount, geometryCapacity));
this.maxSectionCount = maxSectionCount;
public MDICSectionRenderer(ModelStore modelStore, BasicSectionGeometryData geometryData) {
super(modelStore, geometryData);
}
private void uploadUniformBuffer(MDICViewport viewport) {
long ptr = UploadStream.INSTANCE.upload(this.uniform, 0, 1024);
@@ -103,8 +99,8 @@ public class MDICSectionRenderer extends AbstractSectionRenderer<MDICViewport, B
private void bindRenderingBuffers(GlTexture depthBoundTexture) {
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniform.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometryManager.getGeometryBufferId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.geometryManager.getMetadataBufferId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometryManager.getGeometryBuffer().id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.geometryManager.getMetadataBuffer().id);
this.modelStore.bind(3, 4, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, this.positionScratchBuffer.id);
LightMapHelper.bind(1);
@@ -192,7 +188,7 @@ public class MDICSectionRenderer extends AbstractSectionRenderer<MDICViewport, B
this.cullShader.bind();
glBindVertexArray(RenderService.STATIC_VAO);
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniform.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometryManager.getMetadataBufferId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometryManager.getMetadataBuffer().id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, viewport.visibilityBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, viewport.indirectLookupBuffer.id);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, this.drawCountCallBuffer.id);
@@ -213,7 +209,7 @@ public class MDICSectionRenderer extends AbstractSectionRenderer<MDICViewport, B
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniform.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.drawCallBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.drawCountCallBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.geometryManager.getMetadataBufferId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.geometryManager.getMetadataBuffer().id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, viewport.visibilityBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, viewport.indirectLookupBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, this.positionScratchBuffer.id);
@@ -254,17 +250,16 @@ public class MDICSectionRenderer extends AbstractSectionRenderer<MDICViewport, B
@Override
public void addDebug(List<String> 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();

View File

@@ -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) {

View File

@@ -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<SectionMeta> 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<MemoryBuffer> 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<BuiltSection> callback) {
throw new IllegalStateException("Not yet implemented");
}
public Int2ObjectOpenHashMap<MemoryBuffer> 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;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package me.cortex.voxy.client.core.rendering.section.geometry;
public interface IGeometryData {
void free();
}

View File

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

View File

@@ -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) {

View File

@@ -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<<SIZE_BITS)-1;
private final long ADDR_MSK = (1L<<ADDR_BITS)-1;
private static final int ADDR_BITS = 34;//This gives max size per allocation of 2^30 and max address of 2^39
private static final int SIZE_BITS = 64 - ADDR_BITS;
private static final long SIZE_MSK = (1L<<SIZE_BITS)-1;
private static final long ADDR_MSK = (1L<<ADDR_BITS)-1;
private final LongRBTreeSet FREE = new LongRBTreeSet();//Size Address
private final LongRBTreeSet TAKEN = new LongRBTreeSet();//Address Size
private long sizeLimit = Long.MAX_VALUE;
private long totalSize;
//Flags
public boolean resized;//If the required memory of the entire buffer grew
private boolean resized;//If the required memory of the entire buffer grew
//Gets and resets the resized flag
public boolean getResetResized() {
boolean ret = this.resized;
this.resized = false;
return ret;
}
public long getSize() {
return totalSize;
return this.totalSize;
}
/*
public long allocFromLargest(int size) {//Allocates from the largest avalible block, this is useful for expanding later on
@@ -34,34 +44,34 @@ public class AllocationArena {
public long alloc(int size) {//TODO: add alignment support
if (size == 0) throw new IllegalArgumentException();
//This is stupid, iterator is not inclusive
var iter = FREE.iterator(((long) size << ADDR_BITS)-1);
var iter = this.FREE.iterator(((long) size << ADDR_BITS)-1);
if (!iter.hasNext()) {//No free space for allocation
//Create new allocation
resized = true;
long addr = totalSize;
if (totalSize+size>sizeLimit) {
this.resized = true;
long addr = this.totalSize;
if (this.totalSize+size>this.sizeLimit) {
return SIZE_LIMIT;
}
totalSize += size;
TAKEN.add((addr<<SIZE_BITS)|((long) size));
this.totalSize += size;
this.TAKEN.add((addr<<SIZE_BITS)|((long) size));
return addr;
} else {
long slot = iter.nextLong();
iter.remove();
if ((slot >>> ADDR_BITS) == size) {//If the allocation and slot is the same size, just add it to the taken
TAKEN.add((slot<<SIZE_BITS)|(slot >>> ADDR_BITS));
this.TAKEN.add((slot<<SIZE_BITS)|(slot >>> ADDR_BITS));
} else {
TAKEN.add(((slot&ADDR_MSK)<<SIZE_BITS)|size);
FREE.add((((slot >>> ADDR_BITS)-size)<<ADDR_BITS)|((slot&ADDR_MSK)+size));
this.TAKEN.add(((slot&ADDR_MSK)<<SIZE_BITS)|size);
this.FREE.add((((slot >>> ADDR_BITS)-size)<<ADDR_BITS)|((slot&ADDR_MSK)+size));
}
resized = false;
//this.resized = false;
return slot&ADDR_MSK;
}
}
public int free(long addr) {//Returns size of freed memory
addr &= ADDR_MSK;//encase addr stores shit in its upper bits
var iter = TAKEN.iterator(addr<<SIZE_BITS);//Dont need to include -1 as size != 0
var iter = this.TAKEN.iterator(addr<<SIZE_BITS);//Dont need to include -1 as size != 0
long slot = iter.nextLong();
if (slot>>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<<ADDR_BITS)|endAddr);//Free the slot to be merged into
this.FREE.remove((delta<<ADDR_BITS)|endAddr);//Free the slot to be merged into
//Generate a new slot to get put into FREE
slot = (endAddr<<SIZE_BITS) | ((slot&SIZE_MSK) + delta);
}
iter.nextLong();//Need to reset the iter into its state
}//If there is no previous it means were at the start of the buffer, we might need to merge with block 0 if we are not block 0
else if (!FREE.isEmpty()) {// if free is not empty it means we must merge with block of free starting at 0
else if (!this.FREE.isEmpty()) {// if free is not empty it means we must merge with block of free starting at 0
//if (addr != 0)//FIXME: this is very dodgy solution, if addr == 0 it means its impossible for there to be a previous element
if (FREE.remove(addr<<ADDR_BITS)) {//Attempt to remove block 0, this is very dodgy as it assumes block zero is 0 addr n size
if (this.FREE.remove(addr<<ADDR_BITS)) {//Attempt to remove block 0, this is very dodgy as it assumes block zero is 0 addr n size
slot = addr + size;//slot at address 0 and size of 0 block + new block
}
}
@@ -96,20 +106,20 @@ public class AllocationArena {
long endAddr = (slot>>>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<<ADDR_BITS)|endAddr);
this.FREE.remove((delta<<ADDR_BITS)|endAddr);
slot = (slot&(ADDR_MSK<<SIZE_BITS)) | ((slot&SIZE_MSK) + delta);
}
}// if there is no next block it means that we have reached the end of the allocation sections and we can shrink the buffer
else {
resized = true;
totalSize -= (slot&SIZE_MSK);
this.resized = true;
this.totalSize -= (slot&SIZE_MSK);
return (int) size;
}
resized = false;
//this.resized = false;
//Need to swap around the slot to be in FREE format
slot = (slot>>>SIZE_BITS) | (slot<<ADDR_BITS);
FREE.add(slot);//Add the free slot into segments
this.FREE.add(slot);//Add the free slot into segments
return (int) size;
}
@@ -118,7 +128,7 @@ public class AllocationArena {
//Attempts to expand an allocation, returns true on success
public boolean expand(long addr, int extra) {
addr &= ADDR_MSK;//encase addr stores shit in its upper bits
var iter = TAKEN.iterator(addr<<SIZE_BITS);
var iter = this.TAKEN.iterator(addr<<SIZE_BITS);
if (!iter.hasNext()) {
return false;
}
@@ -127,19 +137,19 @@ public class AllocationArena {
throw new IllegalStateException();
}
long updatedSlot = (slot & (ADDR_MSK << SIZE_BITS)) | ((slot & SIZE_MSK) + extra);
resized = false;
//this.resized = false;
if (iter.hasNext()) {
long next = iter.nextLong();
long endAddr = (slot>>>SIZE_BITS)+(slot&SIZE_MSK);
long delta = (next>>>SIZE_BITS) - endAddr;
if (extra <= delta) {
FREE.remove((delta<<ADDR_BITS)|endAddr);//Should assert this
this.FREE.remove((delta<<ADDR_BITS)|endAddr);//Should assert this
iter.previousLong();//FOR SOME REASON NEED TO DO IT TWICE I HAVE NO IDEA WHY
iter.previousLong();
iter.remove();//Remove the allocation so it can be updated
TAKEN.add(updatedSlot);//Update the taken allocation
this.TAKEN.add(updatedSlot);//Update the taken allocation
if (extra != delta) {//More space than needed, need to add a new FREE block
FREE.add(((delta-extra)<<ADDR_BITS)|(endAddr+extra));
this.FREE.add(((delta-extra)<<ADDR_BITS)|(endAddr+extra));
}
//else There is exactly enough free space, so removing the free block and updating the allocation is enough
return true;
@@ -147,19 +157,19 @@ public class AllocationArena {
return false;//Not enough room to expand
}
} else {//We are at the end of the buffer, we can expand as we like
if (totalSize+extra>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");
}
}
}