child emptiness tracking
This commit is contained in:
@@ -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<<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import me.cortex.voxy.common.util.MemoryBuffer;
|
||||
import me.cortex.voxy.common.util.UnsafeUtil;
|
||||
import me.cortex.voxy.common.world.other.Mapper;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -13,7 +14,7 @@ import static org.lwjgl.util.zstd.Zstd.*;
|
||||
|
||||
public class SaveLoadSystem {
|
||||
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) {
|
||||
int x = i&0x1F;
|
||||
@@ -57,6 +58,13 @@ public class SaveLoadSystem {
|
||||
|
||||
long hash = section.key^(lutIndex*1293481298141L);
|
||||
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;
|
||||
for (int i = 0; i < lutIndex; i++) {
|
||||
long id = lutValues[i];
|
||||
@@ -76,12 +84,17 @@ public class SaveLoadSystem {
|
||||
|
||||
public static boolean deserialize(WorldSection section, MemoryBuffer data) {
|
||||
long ptr = data.address;
|
||||
long hash = 0;
|
||||
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;
|
||||
long[] lut = new long[lutLen];
|
||||
long hash = 0;
|
||||
if (VERIFY_HASH_ON_LOAD) {
|
||||
hash = key ^ (lut.length * 1293481298141L);
|
||||
hash ^= metadata; hash *= 1242629872171L;
|
||||
}
|
||||
for (int i = 0; i < lutLen; i++) {
|
||||
lut[i] = MemoryUtil.memGetLong(ptr); ptr += 8;
|
||||
@@ -98,9 +111,13 @@ public class SaveLoadSystem {
|
||||
return false;
|
||||
}
|
||||
|
||||
int nonEmptyBlockCount = 0;
|
||||
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) {
|
||||
long pHash = 99;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package me.cortex.voxy.common.world;
|
||||
|
||||
import me.cortex.voxy.client.Voxy;
|
||||
import me.cortex.voxy.common.voxelization.VoxelizedSection;
|
||||
import me.cortex.voxy.common.world.other.Mapper;
|
||||
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
|
||||
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++) {
|
||||
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() {
|
||||
|
||||
@@ -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<<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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user