child emptiness tracking

This commit is contained in:
mcrcortex
2024-08-08 19:37:38 +10:00
parent b81fd46929
commit 15391a6f91
4 changed files with 183 additions and 39 deletions

View File

@@ -76,6 +76,7 @@ public class VoxelCore {
System.out.println("Voxy core initialized"); System.out.println("Voxy core initialized");
} }
public void enqueueIngest(WorldChunk worldChunk) { public void enqueueIngest(WorldChunk worldChunk) {
this.world.ingestService.enqueueIngest(worldChunk); this.world.ingestService.enqueueIngest(worldChunk);
} }
@@ -219,4 +220,34 @@ public class VoxelCore {
public WorldEngine getWorldEngine() { public WorldEngine getWorldEngine() {
return this.world; 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<<child):0);
section.release();
}
var own = this.world.acquire(lvl, x, y, z);
if (own.getNonEmptyChildren() != msk) {
System.err.println("Section empty child mask not correct " + WorldEngine.pprintPos(own.key) + " got: " + String.format("%8s", Integer.toBinaryString(Byte.toUnsignedInt(own.getNonEmptyChildren()))).replace(' ', '0') + " expected: " + String.format("%8s", Integer.toBinaryString(Byte.toUnsignedInt(msk))).replace(' ', '0'));
}
own.release();
}
}
}
}
}
}
} }

View File

@@ -5,6 +5,7 @@ import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.util.MemoryBuffer;
import me.cortex.voxy.common.util.UnsafeUtil; import me.cortex.voxy.common.util.UnsafeUtil;
import me.cortex.voxy.common.world.other.Mapper;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -13,7 +14,7 @@ import static org.lwjgl.util.zstd.Zstd.*;
public class SaveLoadSystem { public class SaveLoadSystem {
public static final boolean VERIFY_HASH_ON_LOAD = System.getProperty("voxy.verifySectionOnLoad", "true").equals("true"); public static final boolean VERIFY_HASH_ON_LOAD = System.getProperty("voxy.verifySectionOnLoad", "true").equals("true");
public static final int BIGGEST_SERIALIZED_SECTION_SIZE = 32 * 32 * 32 * 8 * 2; public static final int BIGGEST_SERIALIZED_SECTION_SIZE = 32 * 32 * 32 * 8 * 2 + 8;
public static int lin2z(int i) { public static int lin2z(int i) {
int x = i&0x1F; int x = i&0x1F;
@@ -57,6 +58,13 @@ public class SaveLoadSystem {
long hash = section.key^(lutIndex*1293481298141L); long hash = section.key^(lutIndex*1293481298141L);
MemoryUtil.memPutLong(ptr, section.key); ptr += 8; MemoryUtil.memPutLong(ptr, section.key); ptr += 8;
long metadata = 0;
metadata |= Byte.toUnsignedLong(section.nonEmptyChildren);
MemoryUtil.memPutLong(ptr, metadata); ptr += 8;
hash ^= metadata; hash *= 1242629872171L;
MemoryUtil.memPutInt(ptr, lutIndex); ptr += 4; MemoryUtil.memPutInt(ptr, lutIndex); ptr += 4;
for (int i = 0; i < lutIndex; i++) { for (int i = 0; i < lutIndex; i++) {
long id = lutValues[i]; long id = lutValues[i];
@@ -76,12 +84,17 @@ public class SaveLoadSystem {
public static boolean deserialize(WorldSection section, MemoryBuffer data) { public static boolean deserialize(WorldSection section, MemoryBuffer data) {
long ptr = data.address; long ptr = data.address;
long hash = 0;
long key = MemoryUtil.memGetLong(ptr); ptr += 8; long key = MemoryUtil.memGetLong(ptr); ptr += 8;
long metadata = MemoryUtil.memGetLong(ptr); ptr += 8;
section.nonEmptyChildren = (byte) (metadata&0xFF);
int lutLen = MemoryUtil.memGetInt(ptr); ptr += 4; int lutLen = MemoryUtil.memGetInt(ptr); ptr += 4;
long[] lut = new long[lutLen]; long[] lut = new long[lutLen];
long hash = 0;
if (VERIFY_HASH_ON_LOAD) { if (VERIFY_HASH_ON_LOAD) {
hash = key ^ (lut.length * 1293481298141L); hash = key ^ (lut.length * 1293481298141L);
hash ^= metadata; hash *= 1242629872171L;
} }
for (int i = 0; i < lutLen; i++) { for (int i = 0; i < lutLen; i++) {
lut[i] = MemoryUtil.memGetLong(ptr); ptr += 8; lut[i] = MemoryUtil.memGetLong(ptr); ptr += 8;
@@ -98,9 +111,13 @@ public class SaveLoadSystem {
return false; return false;
} }
int nonEmptyBlockCount = 0;
for (int i = 0; i < section.data.length; i++) { for (int i = 0; i < section.data.length; i++) {
section.data[z2lin(i)] = lut[MemoryUtil.memGetShort(ptr)]; ptr += 2; long state = lut[MemoryUtil.memGetShort(ptr)]; ptr += 2;
nonEmptyBlockCount += Mapper.isAir(state)?0:1;
section.data[z2lin(i)] = state;
} }
section.nonEmptyBlockCount = nonEmptyBlockCount;
if (VERIFY_HASH_ON_LOAD) { if (VERIFY_HASH_ON_LOAD) {
long pHash = 99; long pHash = 99;

View File

@@ -1,5 +1,6 @@
package me.cortex.voxy.common.world; package me.cortex.voxy.common.world;
import me.cortex.voxy.client.Voxy;
import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.voxelization.VoxelizedSection;
import me.cortex.voxy.common.world.other.Mapper; import me.cortex.voxy.common.world.other.Mapper;
import me.cortex.voxy.common.world.service.SectionSavingService; import me.cortex.voxy.common.world.service.SectionSavingService;
@@ -114,48 +115,76 @@ public class WorldEngine {
//NOTE: THIS RUNS ON THE THREAD IT WAS EXECUTED ON, when this method exits, the calling method may assume that VoxelizedSection is no longer needed //NOTE: THIS RUNS ON THE THREAD IT WAS EXECUTED ON, when this method exits, the calling method may assume that VoxelizedSection is no longer needed
public void insertUpdate(VoxelizedSection section) {//TODO: add a bitset of levels to update and if it should force update public void insertUpdate(VoxelizedSection section) {//TODO: add a bitset of levels to update and if it should force update
//The >>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++) { 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)); 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 msk = (1<<(lvl+1))-1;
int bx = (section.x&msk)<<(4-lvl); int bx = (section.x&msk)<<(4-lvl);
int by = (section.y&msk)<<(4-lvl); int by = (section.y&msk)<<(4-lvl);
int bz = (section.z&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 y = by; y < (16>>lvl)+by; y++) {
for (int z = bz; z < (16>>lvl)+bz; z++) { for (int z = bz; z < (16>>lvl)+bz; z++) {
for (int x = bx; x < (16>>lvl)+bx; x++) { for (int x = bx; x < (16>>lvl)+bx; x++) {
long newId = section.get(lvl, x-bx, y-by, z-bz); long newId = section.get(lvl, x-bx, y-by, z-bz);
long oldId = worldSection.set(x, y, z, newId); long oldId = worldSection.set(x, y, z, newId);
nonAirCountDelta += Mapper.isAir(oldId)==Mapper.isAir(newId)?0:(Mapper.isAir(newId)?-1:1 ); nonAirCountDelta += Mapper.isAir(oldId)==Mapper.isAir(newId)?0:(Mapper.isAir(newId)?-1:1 );
didChange |= newId != oldId; didStateChange |= newId != oldId;
/*
if (oldId != newId && Mapper.isAir(oldId) && Mapper.isAir(newId)) {
Voxy.breakpoint();
}*/
} }
} }
} }
//Branch into 2 paths, if at lod 0, update the atomic count, if that update resulted in a state transition if (nonAirCountDelta != 0) {
// then aquire the next lod, lock it, recheck our counter, if it is still ok, then atomically update the parent metadata worldSection.addNonEmptyBlockCount(nonAirCountDelta);
//if not lod 0 check that the current occupied state matches the parent lod bit if (lvl == 0) {
// if it doesnt, aquire and lock the next lod level emptinessStateChange = worldSection.updateLvl0State() ? 2 : 0;
// and do the update propagation }
}
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 //Need to release the section after using it
if (didChange) { if (didStateChange||(emptinessStateChange==2)) {
//Mark the section as dirty (enqueuing saving and geometry rebuild) and move to parent mip level if (emptinessStateChange==2) {
this.markDirty(worldSection); //Major state emptiness change, bubble up
worldSection.release(); 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 { } else {
//If nothing changed just need to release, dont need to update parent mips //If nothing changed just need to release, dont need to update parent mips
worldSection.release(); worldSection.release();
break; break;
} }
} }
if (previousSection != null) {
previousSection.release();
}
} }
public int[] getLoadedSectionCacheSizes() { public int[] getLoadedSectionCacheSizes() {

View File

@@ -1,19 +1,37 @@
package me.cortex.voxy.common.world; 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.ArrayDeque;
import java.util.Arrays; import java.util.Arrays;
import java.util.Deque; import java.util.Deque;
import java.util.concurrent.atomic.AtomicBoolean; 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 //Represents a loaded world section at a specific detail level
// holds a 32x32x32 region of detail // holds a 32x32x32 region of detail
public final class WorldSection { public final class WorldSection {
public static final boolean VERIFY_WORLD_SECTION_EXECUTION = System.getProperty("voxy.verifyWorldSectionExecution", "true").equals("true"); 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) //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; private static final int ARRAY_REUSE_CACHE_SIZE = 300;
//TODO: maybe just swap this to a ConcurrentLinkedDeque //TODO: maybe just swap this to a ConcurrentLinkedDeque
@@ -30,13 +48,14 @@ public final class WorldSection {
//Serialized states //Serialized states
long metadata; long metadata;
long[] data = null; long[] data = null;
volatile int nonEmptyBlockCount = 0;
volatile byte nonEmptyChildren;
private final ActiveSectionTracker tracker; private final ActiveSectionTracker tracker;
public final AtomicBoolean inSaveQueue = new AtomicBoolean(); public final AtomicBoolean inSaveQueue = new AtomicBoolean();
//When the first bit is set it means its loaded //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) { WorldSection(int lvl, int x, int y, int z, ActiveSectionTracker tracker) {
this.lvl = lvl; this.lvl = lvl;
@@ -62,17 +81,16 @@ public final class WorldSection {
} }
public boolean tryAcquire() { public boolean tryAcquire() {
int state = this.atomicState.updateAndGet(val -> { int prev, next;
if ((val&1) != 0) { do {
return val+2; prev = (int) ATOMIC_STATE_HANDLE.get(this);
} next = ((prev&1) != 0)?prev+2:prev;
return val; } while (!ATOMIC_STATE_HANDLE.compareAndSet(this, prev, next));
}); return (next&1) != 0;
return (state&1) != 0;
} }
public int acquire() { 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 (VERIFY_WORLD_SECTION_EXECUTION) {
if ((state & 1) == 0) { if ((state & 1) == 0) {
throw new IllegalStateException("Tried to acquire unloaded section"); throw new IllegalStateException("Tried to acquire unloaded section");
@@ -82,12 +100,12 @@ public final class WorldSection {
} }
public int getRefCount() { 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 //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() { 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 (VERIFY_WORLD_SECTION_EXECUTION) {
if (state < 1) { if (state < 1) {
throw new IllegalStateException("Section got into an invalid state"); throw new IllegalStateException("Section got into an invalid state");
@@ -104,7 +122,7 @@ public final class WorldSection {
//Returns true on success, false on failure //Returns true on success, false on failure
boolean trySetFreed() { 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 (VERIFY_WORLD_SECTION_EXECUTION) {
if ((witness & 1) == 0 && witness != 0) { if ((witness & 1) == 0 && witness != 0) {
throw new IllegalStateException("Section marked as free but has refs"); throw new IllegalStateException("Section marked as free but has refs");
@@ -124,7 +142,7 @@ public final class WorldSection {
public void assertNotFree() { public void assertNotFree() {
if (VERIFY_WORLD_SECTION_EXECUTION) { if (VERIFY_WORLD_SECTION_EXECUTION) {
if ((this.atomicState.get() & 1) == 0) { if ((((int) ATOMIC_STATE_HANDLE.get(this)) & 1) == 0) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
} }
@@ -158,6 +176,55 @@ public final class WorldSection {
if (cache.length != this.data.length) throw new IllegalArgumentException(); if (cache.length != this.data.length) throw new IllegalArgumentException();
System.arraycopy(this.data, 0, cache, 0, this.data.length); 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<<childIdx);
byte prev, next;
do {
prev = this.getNonEmptyChildren();
next = (byte) ((prev&(~msk))|(child.getNonEmptyChildren()!=0?msk:0));
} while (!NON_EMPTY_CHILD_HANDLE.compareAndSet(this, prev, next));
return ((prev!=0)^(next!=0))?2:(prev!=next?1:0);
}
public int getNonEmptyBlockCount() {
return (int) NON_EMPTY_BLOCK_HANDLE.get(this);
}
public int addNonEmptyBlockCount(int delta) {
if (VERIFY_WORLD_SECTION_EXECUTION) {
if (this.lvl != 0) {
throw new IllegalStateException("Tried updating a level 0 lod when its not level 0: " + WorldEngine.pprintPos(this.key));
}
}
int count = ((int)NON_EMPTY_BLOCK_HANDLE.getAndAdd(this, delta)) + delta;
if (VERIFY_WORLD_SECTION_EXECUTION) {
if (count < 0) {
throw new IllegalStateException("Count is negative!");
}
}
return count;
}
public boolean updateLvl0State() {
byte prev, next;
do {
prev = this.getNonEmptyChildren();
next = (byte) (((int)NON_EMPTY_BLOCK_HANDLE.get(this))==0?0:0xFF);
} while (!NON_EMPTY_CHILD_HANDLE.compareAndSet(this, prev, next));
return prev != next;
}
}