From 5d01a37192c774eb7d3b1d945a932f61b9ff5c9f Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:52:01 +1000 Subject: [PATCH] Section tracker core rewrite --- .../core/rendering/RenderTracker.java | 2 +- .../rendering/building/RenderDataFactory.java | 20 ++- .../building/RenderGenerationService.java | 2 +- .../voxelmon/core/util/VolatileHolder.java | 5 + .../core/world/ActiveSectionTracker.java | 99 +++++++++++ .../voxelmon/core/world/SaveLoadSystem.java | 10 +- .../voxelmon/core/world/WorldEngine.java | 162 +++--------------- .../voxelmon/core/world/WorldSection.java | 96 +++++------ 8 files changed, 189 insertions(+), 207 deletions(-) create mode 100644 src/main/java/me/cortex/voxelmon/core/util/VolatileHolder.java create mode 100644 src/main/java/me/cortex/voxelmon/core/world/ActiveSectionTracker.java diff --git a/src/main/java/me/cortex/voxelmon/core/rendering/RenderTracker.java b/src/main/java/me/cortex/voxelmon/core/rendering/RenderTracker.java index cef0d72c..345ef715 100644 --- a/src/main/java/me/cortex/voxelmon/core/rendering/RenderTracker.java +++ b/src/main/java/me/cortex/voxelmon/core/rendering/RenderTracker.java @@ -54,7 +54,7 @@ public class RenderTracker { continue; for (int y = -3>>i; y < Math.max(1, 10 >> i); y++) { - var sec = this.world.getOrLoadAcquire(i, x + (OX>>(1+i)), y, z + (OZ>>(1+i))); + var sec = this.world.acquire(i, x + (OX>>(1+i)), y, z + (OZ>>(1+i))); //this.renderGen.enqueueTask(sec); sec.release(); } diff --git a/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderDataFactory.java b/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderDataFactory.java index d1d84e60..a8470073 100644 --- a/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderDataFactory.java +++ b/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderDataFactory.java @@ -38,9 +38,11 @@ public class RenderDataFactory { // appearing between lods - if (section.definitelyEmpty()) { - return new BuiltSectionGeometry(section.getKey(), null, null); - } + //if (section.definitelyEmpty()) {//Fast path if its known the entire chunk is empty + // return new BuiltSectionGeometry(section.getKey(), null, null); + //} + + var data = section.copyData(); long[] connectedData = null; @@ -64,7 +66,7 @@ public class RenderDataFactory { if (y == 31 && ((buildMask>>(6+dirId))&1) == 0) { //Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies if (connectedData == null) { - var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y + 1, section.z); + var connectedSection = this.world.acquire(section.lvl, section.x, section.y + 1, section.z); connectedData = connectedSection.copyData(); connectedSection.release(); } @@ -106,7 +108,7 @@ public class RenderDataFactory { if (x == 31 && ((buildMask>>(6+dirId))&1) == 0) { //Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies if (connectedData == null) { - var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x + 1, section.y, section.z); + var connectedSection = this.world.acquire(section.lvl, section.x + 1, section.y, section.z); connectedData = connectedSection.copyData(); connectedSection.release(); } @@ -148,7 +150,7 @@ public class RenderDataFactory { if (z == 31 && ((buildMask>>(6+dirId))&1) == 0) { //Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies if (connectedData == null) { - var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y, section.z + 1); + var connectedSection = this.world.acquire(section.lvl, section.x, section.y, section.z + 1); connectedData = connectedSection.copyData(); connectedSection.release(); } @@ -190,7 +192,7 @@ public class RenderDataFactory { if (x == 0 && ((buildMask>>(6+dirId))&1) == 0) { //Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies if (connectedData == null) { - var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x - 1, section.y, section.z); + var connectedSection = this.world.acquire(section.lvl, section.x - 1, section.y, section.z); connectedData = connectedSection.copyData(); connectedSection.release(); } @@ -232,7 +234,7 @@ public class RenderDataFactory { if (z == 0 && ((buildMask>>(6+dirId))&1) == 0) { //Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies if (connectedData == null) { - var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y, section.z - 1); + var connectedSection = this.world.acquire(section.lvl, section.x, section.y, section.z - 1); connectedData = connectedSection.copyData(); connectedSection.release(); } @@ -274,7 +276,7 @@ public class RenderDataFactory { if (y == 0 && ((buildMask>>(6+dirId))&1) == 0) { //Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies if (connectedData == null) { - var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y - 1, section.z); + var connectedSection = this.world.acquire(section.lvl, section.x, section.y - 1, section.z); connectedData = connectedSection.copyData(); connectedSection.release(); } diff --git a/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderGenerationService.java b/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderGenerationService.java index 3249d5a9..98548dbc 100644 --- a/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderGenerationService.java +++ b/src/main/java/me/cortex/voxelmon/core/rendering/building/RenderGenerationService.java @@ -81,7 +81,7 @@ public class RenderGenerationService { public void enqueueTask(int lvl, int x, int y, int z) { this.taskQueue.add(new BuildTask(()->{ if (this.tracker.shouldStillBuild(lvl, x, y, z)) { - return this.world.getOrLoadAcquire(lvl, x, y, z); + return this.world.acquire(lvl, x, y, z); } else { return null; } diff --git a/src/main/java/me/cortex/voxelmon/core/util/VolatileHolder.java b/src/main/java/me/cortex/voxelmon/core/util/VolatileHolder.java new file mode 100644 index 00000000..d394bd98 --- /dev/null +++ b/src/main/java/me/cortex/voxelmon/core/util/VolatileHolder.java @@ -0,0 +1,5 @@ +package me.cortex.voxelmon.core.util; + +public class VolatileHolder { + public T obj; +} diff --git a/src/main/java/me/cortex/voxelmon/core/world/ActiveSectionTracker.java b/src/main/java/me/cortex/voxelmon/core/world/ActiveSectionTracker.java new file mode 100644 index 00000000..26d1c68e --- /dev/null +++ b/src/main/java/me/cortex/voxelmon/core/world/ActiveSectionTracker.java @@ -0,0 +1,99 @@ +package me.cortex.voxelmon.core.world; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import me.cortex.voxelmon.core.util.VolatileHolder; + +import java.lang.ref.Reference; + +public class ActiveSectionTracker { + //Deserialize into the supplied section, returns true on success, false on failure + public interface SectionLoader {boolean load(WorldSection section);} + + //Loaded section world cache, TODO: get rid of VolatileHolder and use something more sane + + private final Long2ObjectOpenHashMap>[] loadedSectionCache; + private final SectionLoader loader; + public ActiveSectionTracker(int layers, SectionLoader loader) { + this.loader = loader; + this.loadedSectionCache = new Long2ObjectOpenHashMap[layers]; + for (int i = 0; i < layers; i++) { + this.loadedSectionCache[i] = new Long2ObjectOpenHashMap<>(1<<(16-i)); + } + } + + public WorldSection acquire(int lvl, int x, int y, int z) { + long key = WorldEngine.getWorldSectionId(lvl, x, y, z); + var cache = this.loadedSectionCache[lvl]; + VolatileHolder holder = null; + boolean isLoader = false; + synchronized (cache) { + holder = cache.get(key); + if (holder == null) { + holder = new VolatileHolder<>(); + cache.put(key, holder); + isLoader = true; + } + var section = holder.obj; + if (section != null) { + section.acquire(); + return section; + } + } + //If this thread was the one to create the reference then its the thread to load the section + if (isLoader) { + var section = new WorldSection(lvl, x, y, z, this); + if (!this.loader.load(section)) { + //TODO: Instead if throwing an exception do something better + throw new IllegalStateException("Unable to load section"); + } + section.acquire(); + holder.obj = section; + return section; + } else { + WorldSection section = null; + while ((section = holder.obj) == null) + Thread.onSpinWait(); + + synchronized (cache) { + if (section.tryAcquire()) { + return section; + } + } + return this.acquire(lvl, x, y, z); + } + } + + void tryUnload(WorldSection section) { + var cache = this.loadedSectionCache[section.lvl]; + synchronized (cache) { + if (section.trySetFreed()) { + if (cache.remove(section.getKey()).obj != section) { + throw new IllegalStateException("Removed section not the same as the referenced section in the cache"); + } + } + } + } + + + public int[] getCacheCounts() { + int[] res = new int[this.loadedSectionCache.length]; + for (int i = 0; i < this.loadedSectionCache.length; i++) { + res[i] = this.loadedSectionCache[i].size(); + } + return res; + } + + + public static void main(String[] args) { + var tracker = new ActiveSectionTracker(1, a->true); + + var section = tracker.acquire(0,0,0,0); + section.acquire(); + var section2 = tracker.acquire(0,0,0,0); + section.release(); + section.release(); + section = tracker.acquire(0,0,0,0); + section.release(); + + } +} diff --git a/src/main/java/me/cortex/voxelmon/core/world/SaveLoadSystem.java b/src/main/java/me/cortex/voxelmon/core/world/SaveLoadSystem.java index 94454418..942409ad 100644 --- a/src/main/java/me/cortex/voxelmon/core/world/SaveLoadSystem.java +++ b/src/main/java/me/cortex/voxelmon/core/world/SaveLoadSystem.java @@ -68,7 +68,7 @@ public class SaveLoadSystem { return out; } - public static WorldSection deserialize(WorldEngine world, int lvl, int x, int y, int z, byte[] data) { + public static boolean deserialize(WorldSection section, byte[] data) { var buff = MemoryUtil.memAlloc(data.length); buff.put(data); buff.rewind(); @@ -89,8 +89,6 @@ public class SaveLoadSystem { hash ^= lut[i]; } - var section = new WorldSection(lvl, x, y, z, world); - section.definitelyEmpty = false; if (section.getKey() != key) { throw new IllegalStateException("Decompressed section not the same as requested. got: " + key + " expected: " + section.getKey()); } @@ -107,16 +105,16 @@ public class SaveLoadSystem { if (expectedHash != hash) { //throw new IllegalStateException("Hash mismatch got: " + hash + " expected: " + expectedHash); System.err.println("Hash mismatch got: " + hash + " expected: " + expectedHash + " removing region"); - return null; + return false; } if (decompressed.hasRemaining()) { //throw new IllegalStateException("Decompressed section had excess data"); System.err.println("Decompressed section had excess data removing region"); - return null; + return false; } MemoryUtil.memFree(decompressed); - return section; + return true; } } diff --git a/src/main/java/me/cortex/voxelmon/core/world/WorldEngine.java b/src/main/java/me/cortex/voxelmon/core/world/WorldEngine.java index 0c7f33bc..81bc4a4e 100644 --- a/src/main/java/me/cortex/voxelmon/core/world/WorldEngine.java +++ b/src/main/java/me/cortex/voxelmon/core/world/WorldEngine.java @@ -9,6 +9,7 @@ import me.cortex.voxelmon.core.world.service.VoxelIngestService; import me.cortex.voxelmon.core.world.storage.StorageBackend; import java.io.File; +import java.util.Arrays; import java.util.Deque; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicReference; @@ -20,6 +21,7 @@ public class WorldEngine { public final StorageBackend storage; private final Mapper mapper; + private final ActiveSectionTracker sectionTracker; public final VoxelIngestService ingestService = new VoxelIngestService(this); public final SectionSavingService savingService; private RenderTracker renderTracker; @@ -32,152 +34,40 @@ public class WorldEngine { private final int maxMipLevels; - //Loaded section world cache - private final Long2ObjectOpenHashMap[] loadedSectionCache; - //TODO: also segment this up into an array - private final Long2ObjectOpenHashMap> sectionLoadingLocks = new Long2ObjectOpenHashMap<>(); - - //What this is used for is to keep N sections acquired per layer, this stops sections from constantly being - // loaded and unloaded when accessed close together - private final ConcurrentLinkedDeque[] activeSectionCache; - public WorldEngine(File storagePath, int savingServiceWorkers, int maxMipLayers) { this.maxMipLevels = maxMipLayers; - this.loadedSectionCache = new Long2ObjectOpenHashMap[maxMipLayers]; - this.activeSectionCache = new ConcurrentLinkedDeque[maxMipLayers]; - for (int i = 0; i < maxMipLayers; i++) { - this.loadedSectionCache[i] = new Long2ObjectOpenHashMap<>(1<<(16-i)); - this.activeSectionCache[i] = new ConcurrentLinkedDeque<>(); - } this.storage = new StorageBackend(storagePath); this.mapper = new Mapper(this.storage); + this.sectionTracker = new ActiveSectionTracker(maxMipLayers, this::unsafeLoadSection); + this.savingService = new SectionSavingService(this, savingServiceWorkers); } + private boolean unsafeLoadSection(WorldSection into) { + var data = this.storage.getSectionData(into.getKey()); + if (data != null) { + if (!SaveLoadSystem.deserialize(into, data)) { + this.storage.deleteSectionData(into.getKey()); + //TODO: regenerate the section from children + Arrays.fill(into.data, Mapper.AIR); + System.err.println("Section " + into.lvl + ", " + into.x + ", " + into.y + ", " + into.z + " was unable to load, setting to air"); + return true; + } + } + return true; + } + + public WorldSection acquire(int lvl, int x, int y, int z) { + return this.sectionTracker.acquire(lvl, x, y, z); + } + //TODO: Fixme/optimize, cause as the lvl gets higher, the size of x,y,z gets smaller so i can dynamically compact the format // depending on the lvl, which should optimize colisions and whatnot public static long getWorldSectionId(int lvl, int x, int y, int z) { return ((long)lvl<<60)|((long)(y&0xFF)<<52)|((long)(z&((1<<24)-1))<<28)|((long)(x&((1<<24)-1))<<4);//NOTE: 4 bits spare for whatever } - public static int getLvl(long packed) { - return (int) (packed>>>60); - } - public static int getX(long packed) { - return (int) ((packed<<12)>>40); - } - public static int getY(long packed) { - return (int) ((packed<<4)>>56); - } - public static int getZ(long packed) { - return (int) ((packed<<4)>>40); - } - - //Try to unload the section from the world atomically, this is called from the saving service, or any release call which results in the refcount being 0 - public void tryUnload(WorldSection section) { - synchronized (this.loadedSectionCache[section.lvl]) { - if (section.getRefCount() != 0) { - section.assertNotFree(); - return; - } - - //TODO: make a thing where it checks if the section is dirty, if it is, enqueue it for a save first and return - - section.setFreed();//TODO: FIXME THIS IS SOMEHOW FAILING - var removedSection = this.loadedSectionCache[section.lvl].remove(section.getKey()); - if (removedSection != section) { - throw new IllegalStateException("Removed section not the same as attempted to remove"); - } - if (section.isAcquired()) { - throw new IllegalStateException("Section that was just removed got reacquired"); - } - } - } - - //Internal helper method for getOrLoad to segment up code - private WorldSection unsafeLoadSection(long key, int lvl, int x, int y, int z) { - var data = this.storage.getSectionData(key); - if (data == null) { - return new WorldSection(lvl, x, y, z, this); - } else { - var ret = SaveLoadSystem.deserialize(this, lvl, x, y, z, data); - if (ret != null) { - return ret; - } else { - this.storage.deleteSectionData(key); - return new WorldSection(lvl, x, y, z, this); - } - } - } - - //Gets a loaded section or loads the section from storage - public WorldSection getOrLoadAcquire(int lvl, int x, int y, int z) { - long key = getWorldSectionId(lvl, x, y, z); - - AtomicReference lock = null; - AtomicReference gotLock = null; - synchronized (this.loadedSectionCache[lvl]) { - var result = this.loadedSectionCache[lvl].get(key); - if (result != null) { - result.acquire(); - return result; - } - lock = new AtomicReference<>(null); - synchronized (this.sectionLoadingLocks) { - var finalLock = lock; - gotLock = this.sectionLoadingLocks.computeIfAbsent(key, a -> finalLock); - } - } - - //We acquired the lock so load it - if (gotLock == lock) { - WorldSection loadedSection = this.unsafeLoadSection(key, lvl, x, y, z); - loadedSection.acquire(); - - - //Insert the loaded section and set the loading lock to the loaded value - synchronized (this.loadedSectionCache[lvl]) { - this.loadedSectionCache[lvl].put(key, loadedSection); - synchronized (this.sectionLoadingLocks) { - this.sectionLoadingLocks.remove(key); - lock.set(loadedSection); - } - } - - //Add to the active acquired cache and remove the last item if the size is over the limit - { - loadedSection.acquire(); - this.activeSectionCache[lvl].add(loadedSection); - if (this.activeSectionCache[lvl].size() > ACTIVE_CACHE_SIZE) { - var last = this.activeSectionCache[lvl].pop(); - last.release(); - } - } - - return loadedSection; - } else { - lock = gotLock; - //Another thread got the lock so spin wait for the section to load - while (lock.get() == null) { - Thread.onSpinWait(); - } - var section = lock.get(); - //Fixme: try find a better solution for this - - //The issue with this is that the section could be unloaded when we acquire it cause of so many threading pain - // so lock the section cache, try acquire the section, if we fail we must load the section again - synchronized (this.loadedSectionCache[lvl]) { - if (section.tryAcquire()) { - //We acquired the section successfully, return it - return section; - } - } - //We failed to acquire the section, we must reload it - return this.getOrLoadAcquire(lvl, x, y, z); - } - } - //Marks a section as dirty, enqueuing it for saving and or render data rebuilding private void markDirty(WorldSection section) { this.renderTracker.sectionUpdated(section); @@ -191,7 +81,7 @@ public class WorldEngine { public void insertUpdate(VoxelizedSection section) { //The >>1 is cause the world sections size is 32x32x32 vs the 16x16x16 of the voxelized section for (int lvl = 0; lvl < this.maxMipLevels; lvl++) { - var worldSection = this.getOrLoadAcquire(lvl, section.x>>(lvl+1), section.y>>(lvl+1), section.z>>(lvl+1)); + var worldSection = this.acquire(lvl, section.x >> (lvl + 1), section.y >> (lvl + 1), section.z >> (lvl + 1)); int msk = (1<<(lvl+1))-1; int bx = (section.x&msk)<<(4-lvl); int by = (section.y&msk)<<(4-lvl); @@ -221,11 +111,7 @@ public class WorldEngine { } public int[] getLoadedSectionCacheSizes() { - var res = new int[this.maxMipLevels]; - for (int i = 0; i < this.maxMipLevels; i++) { - res[i] = this.loadedSectionCache[i].size(); - } - return res; + return this.sectionTracker.getCacheCounts(); } public void shutdown() { diff --git a/src/main/java/me/cortex/voxelmon/core/world/WorldSection.java b/src/main/java/me/cortex/voxelmon/core/world/WorldSection.java index 909d9628..4d0c9b8f 100644 --- a/src/main/java/me/cortex/voxelmon/core/world/WorldSection.java +++ b/src/main/java/me/cortex/voxelmon/core/world/WorldSection.java @@ -13,20 +13,21 @@ public final class WorldSection { public final int y; public final int z; - ////Maps from a local id to global meaning it should be much cheaper to store in memory probably - //private final int[] dataMapping = null; - //private final short[] data = new short[32*32*32]; - final long[] data = new long[32*32*32]; - boolean definitelyEmpty = true; + long[] data; + private final ActiveSectionTracker tracker; + public final AtomicBoolean inSaveQueue = new AtomicBoolean(); - private final WorldEngine world; + //When the first bit is set it means its loaded + private final AtomicInteger atomicState = new AtomicInteger(1); - public WorldSection(int lvl, int x, int y, int z, WorldEngine worldIn) { + WorldSection(int lvl, int x, int y, int z, ActiveSectionTracker tracker) { this.lvl = lvl; this.x = x; this.y = y; this.z = z; - this.world = worldIn; + this.tracker = tracker; + + this.data = new long[32*32*32]; } @Override @@ -34,65 +35,49 @@ public final class WorldSection { return ((x*1235641+y)*8127451+z)*918267913+lvl; } - public final AtomicBoolean inSaveQueue = new AtomicBoolean(); - private final AtomicInteger usageCounts = new AtomicInteger(); public int acquire() { - this.assertNotFree(); - return this.usageCounts.getAndAdd(1); - } - - //TODO: Fixme i dont think this is fully thread safe/correct - public boolean tryAcquire() { - if (this.freed) { - return false; + int state = this.atomicState.addAndGet(2); + if ((state&1) == 0) { + throw new IllegalStateException("Tried to acquire unloaded section"); } - this.usageCounts.getAndAdd(1); - if (this.freed) { - return false; - } - return true; + return state>>1; } public int release() { - this.assertNotFree(); - int i = this.usageCounts.addAndGet(-1); - if (i < 0) { - throw new IllegalStateException(); + int state = this.atomicState.addAndGet(-2); + if (state < 1) { + throw new IllegalStateException("Section got into an invalid state"); + } + if ((state&1)==0) { + throw new IllegalStateException("Tried releasing a freed section"); + } + if ((state>>1)==0) { + this.tracker.tryUnload(this); } - - //NOTE: cant actually check for not free as at this stage it technically could be unloaded, as soon - //this.assertNotFree(); - - - //Try to unload the section if its empty - if (i == 0) { - this.world.tryUnload(this); - } - return i; + return state>>1; } - private volatile boolean freed = false; - void setFreed() { - this.assertNotFree(); - this.freed = true; + //Returns true on success, false on failure + boolean trySetFreed() { + int witness = this.atomicState.compareAndExchange(1, 0); + if ((witness&1)==0 && witness != 0) { + throw new IllegalStateException("Section marked as free but has refs"); + } + boolean isFreed = witness == 1; + if (isFreed) { + this.data = null; + } + return isFreed; } public void assertNotFree() { - if (this.freed) { + if ((this.atomicState.get() & 1) == 0) { throw new IllegalStateException(); } } - public boolean isAcquired() { - return this.usageCounts.get() != 0; - } - - public int getRefCount() { - return this.usageCounts.get(); - } - public long getKey() { return WorldEngine.getWorldSectionId(this.lvl, this.x, this.y, this.z); } @@ -114,11 +99,18 @@ public final class WorldSection { //Generates a copy of the data array, this is to help with atomic operations like rendering public long[] copyData() { + this.assertNotFree(); return Arrays.copyOf(this.data, this.data.length); } - public boolean definitelyEmpty() { - return this.definitelyEmpty; + public boolean tryAcquire() { + int state = this.atomicState.updateAndGet(val -> { + if ((val&1) != 0) { + return val+2; + } + return val; + }); + return (state&1) != 0; } }