From 15391a6f911e4822d07b9bae55f6002222dfd701 Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:37:38 +1000 Subject: [PATCH] child emptiness tracking --- .../me/cortex/voxy/client/core/VoxelCore.java | 31 ++++++ .../voxy/common/world/SaveLoadSystem.java | 23 +++- .../cortex/voxy/common/world/WorldEngine.java | 63 ++++++++--- .../voxy/common/world/WorldSection.java | 105 ++++++++++++++---- 4 files changed, 183 insertions(+), 39 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java index 0bfd9040..e7b06a66 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java @@ -76,6 +76,7 @@ public class VoxelCore { System.out.println("Voxy core initialized"); } + public void enqueueIngest(WorldChunk worldChunk) { this.world.ingestService.enqueueIngest(worldChunk); } @@ -219,4 +220,34 @@ public class VoxelCore { public WorldEngine getWorldEngine() { return this.world; } + + private void verifyTopNodeChildren(int X, int Y, int Z) { + for (int lvl = 0; lvl < 5; lvl++) { + for (int y = (Y<<5)>>lvl; y < ((Y+1)<<5)>>lvl; y++) { + for (int x = (X<<5)>>lvl; x < ((X+1)<<5)>>lvl; x++) { + for (int z = (Z<<5)>>lvl; z < ((Z+1)<<5)>>lvl; z++) { + if (lvl == 0) { + var own = this.world.acquire(lvl, x, y, z); + if ((own.getNonEmptyChildren() != 0) ^ (own.getNonEmptyBlockCount() != 0)) { + System.err.println("Lvl 0 node not marked correctly " + WorldEngine.pprintPos(own.key)); + } + own.release(); + } else { + byte msk = 0; + for (int child = 0; child < 8; child++) { + var section = this.world.acquire(lvl-1, (child&1)+(x<<1), ((child>>2)&1)+(y<<1), ((child>>1)&1)+(z<<1)); + msk |= (byte) (section.getNonEmptyBlockCount()!=0?(1<>1 is cause the world sections size is 32x32x32 vs the 16x16x16 of the voxelized section + boolean shouldCheckEmptiness = false; + WorldSection previousSection = null; + for (int lvl = 0; lvl < this.maxMipLevels; lvl++) { - int nonAirCountDelta = 0; var worldSection = this.acquire(lvl, section.x >> (lvl + 1), section.y >> (lvl + 1), section.z >> (lvl + 1)); + + int emptinessStateChange = 0; + //Propagate the child existence state of the previous iteration to this section + if (lvl != 0 && shouldCheckEmptiness) { + emptinessStateChange = worldSection.updateEmptyChildState(previousSection); + //We kept the previous section acquired, so we need to release it + previousSection.release(); + previousSection = null; + } + + int msk = (1<<(lvl+1))-1; int bx = (section.x&msk)<<(4-lvl); int by = (section.y&msk)<<(4-lvl); int bz = (section.z&msk)<<(4-lvl); - boolean didChange = false; + + int nonAirCountDelta = 0; + boolean didStateChange = false; for (int y = by; y < (16>>lvl)+by; y++) { for (int z = bz; z < (16>>lvl)+bz; z++) { for (int x = bx; x < (16>>lvl)+bx; x++) { long newId = section.get(lvl, x-bx, y-by, z-bz); long oldId = worldSection.set(x, y, z, newId); nonAirCountDelta += Mapper.isAir(oldId)==Mapper.isAir(newId)?0:(Mapper.isAir(newId)?-1:1 ); - didChange |= newId != oldId; - /* - if (oldId != newId && Mapper.isAir(oldId) && Mapper.isAir(newId)) { - Voxy.breakpoint(); - }*/ + didStateChange |= newId != oldId; } } } - //Branch into 2 paths, if at lod 0, update the atomic count, if that update resulted in a state transition - // then aquire the next lod, lock it, recheck our counter, if it is still ok, then atomically update the parent metadata - //if not lod 0 check that the current occupied state matches the parent lod bit - // if it doesnt, aquire and lock the next lod level - // and do the update propagation + if (nonAirCountDelta != 0) { + worldSection.addNonEmptyBlockCount(nonAirCountDelta); + if (lvl == 0) { + emptinessStateChange = worldSection.updateLvl0State() ? 2 : 0; + } + } + if (didStateChange||(emptinessStateChange!=0)) { + //Mark the section as dirty (enqueuing saving and geometry rebuild) and move to parent mip level + //TODO: have an update type! so that then e.g. if the child empty set changes it doesnt cause chunk rebuilds! + this.markDirty(worldSection); + } //Need to release the section after using it - if (didChange) { - //Mark the section as dirty (enqueuing saving and geometry rebuild) and move to parent mip level - this.markDirty(worldSection); - worldSection.release(); + if (didStateChange||(emptinessStateChange==2)) { + if (emptinessStateChange==2) { + //Major state emptiness change, bubble up + shouldCheckEmptiness = true; + //Dont release the section, it will be released on the next loop + previousSection = worldSection; + } else { + //Propagate up without state change + shouldCheckEmptiness = false; + previousSection = null; + worldSection.release(); + } } else { //If nothing changed just need to release, dont need to update parent mips worldSection.release(); break; } } + + if (previousSection != null) { + previousSection.release(); + } } public int[] getLoadedSectionCacheSizes() { diff --git a/src/main/java/me/cortex/voxy/common/world/WorldSection.java b/src/main/java/me/cortex/voxy/common/world/WorldSection.java index 6290a704..39799831 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldSection.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldSection.java @@ -1,19 +1,37 @@ package me.cortex.voxy.common.world; +import me.cortex.voxy.client.Voxy; +import net.minecraft.util.Pair; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; //Represents a loaded world section at a specific detail level // holds a 32x32x32 region of detail public final class WorldSection { public static final boolean VERIFY_WORLD_SECTION_EXECUTION = System.getProperty("voxy.verifyWorldSectionExecution", "true").equals("true"); + + private static final VarHandle ATOMIC_STATE_HANDLE; + private static final VarHandle NON_EMPTY_CHILD_HANDLE; + private static final VarHandle NON_EMPTY_BLOCK_HANDLE; + + static { + try { + ATOMIC_STATE_HANDLE = MethodHandles.lookup().findVarHandle(WorldSection.class, "atomicState", int.class); + NON_EMPTY_CHILD_HANDLE = MethodHandles.lookup().findVarHandle(WorldSection.class, "nonEmptyChildren", byte.class); + NON_EMPTY_BLOCK_HANDLE = MethodHandles.lookup().findVarHandle(WorldSection.class, "nonEmptyBlockCount", int.class); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + //TODO: should make it dynamically adjust the size allowance based on memory pressure/WorldSection allocation rate (e.g. is it doing a world import) private static final int ARRAY_REUSE_CACHE_SIZE = 300; //TODO: maybe just swap this to a ConcurrentLinkedDeque @@ -30,13 +48,14 @@ public final class WorldSection { //Serialized states long metadata; long[] data = null; - + volatile int nonEmptyBlockCount = 0; + volatile byte nonEmptyChildren; private final ActiveSectionTracker tracker; public final AtomicBoolean inSaveQueue = new AtomicBoolean(); //When the first bit is set it means its loaded - private final AtomicInteger atomicState = new AtomicInteger(1); + private volatile int atomicState = 1; WorldSection(int lvl, int x, int y, int z, ActiveSectionTracker tracker) { this.lvl = lvl; @@ -62,17 +81,16 @@ public final class WorldSection { } public boolean tryAcquire() { - int state = this.atomicState.updateAndGet(val -> { - if ((val&1) != 0) { - return val+2; - } - return val; - }); - return (state&1) != 0; + int prev, next; + do { + prev = (int) ATOMIC_STATE_HANDLE.get(this); + next = ((prev&1) != 0)?prev+2:prev; + } while (!ATOMIC_STATE_HANDLE.compareAndSet(this, prev, next)); + return (next&1) != 0; } public int acquire() { - int state = this.atomicState.addAndGet(2); + int state =((int) ATOMIC_STATE_HANDLE.getAndAdd(this, 2)) + 2; if (VERIFY_WORLD_SECTION_EXECUTION) { if ((state & 1) == 0) { throw new IllegalStateException("Tried to acquire unloaded section"); @@ -82,12 +100,12 @@ public final class WorldSection { } public int getRefCount() { - return this.atomicState.get()>>1; + return ((int)ATOMIC_STATE_HANDLE.get(this))>>1; } //TODO: add the ability to hint to the tracker that yes the section is unloaded, try to cache it in a secondary cache since it will be reused/needed later public int release() { - int state = this.atomicState.addAndGet(-2); + int state = ((int) ATOMIC_STATE_HANDLE.getAndAdd(this, -2)) - 2; if (VERIFY_WORLD_SECTION_EXECUTION) { if (state < 1) { throw new IllegalStateException("Section got into an invalid state"); @@ -104,7 +122,7 @@ public final class WorldSection { //Returns true on success, false on failure boolean trySetFreed() { - int witness = this.atomicState.compareAndExchange(1, 0); + int witness = (int) ATOMIC_STATE_HANDLE.compareAndExchange(this, 1, 0); if (VERIFY_WORLD_SECTION_EXECUTION) { if ((witness & 1) == 0 && witness != 0) { throw new IllegalStateException("Section marked as free but has refs"); @@ -124,7 +142,7 @@ public final class WorldSection { public void assertNotFree() { if (VERIFY_WORLD_SECTION_EXECUTION) { - if ((this.atomicState.get() & 1) == 0) { + if ((((int) ATOMIC_STATE_HANDLE.get(this)) & 1) == 0) { throw new IllegalStateException(); } } @@ -158,6 +176,55 @@ public final class WorldSection { if (cache.length != this.data.length) throw new IllegalArgumentException(); System.arraycopy(this.data, 0, cache, 0, this.data.length); } -} -//TODO: for serialization, make a huffman encoding tree on the integers since that should be very very efficent for compression + public static int getChildIndex(int x, int y, int z) { + return (x&1)|((y&1)<<2)|((z&1)<<1); + } + + public byte getNonEmptyChildren() { + return (byte) NON_EMPTY_CHILD_HANDLE.get(this); + } + + //Updates this.nonEmptyChildren atomically with respect to the child passed in + // returns 0 if no change, 1 if it just updated and didnt do a major state change, 2 if it was a major state change (something -> nothing, nothing -> something) + public int updateEmptyChildState(WorldSection child) { + int childIdx = getChildIndex(child.x, child.y, child.z); + byte msk = (byte) (1<