diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/ISectionWatcher.java b/src/main/java/me/cortex/voxy/client/core/rendering/ISectionWatcher.java new file mode 100644 index 00000000..78fb2b3d --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/ISectionWatcher.java @@ -0,0 +1,17 @@ +package me.cortex.voxy.client.core.rendering; + +import me.cortex.voxy.common.world.WorldEngine; + +public interface ISectionWatcher { + default boolean watch(int lvl, int x, int y, int z, int types) { + return this.watch(WorldEngine.getWorldSectionId(lvl, x, y, z), types); + } + + boolean watch(long position, int types); + + default boolean unwatch(int lvl, int x, int y, int z, int types) { + return this.unwatch(WorldEngine.getWorldSectionId(lvl, x, y, z), types); + } + + boolean unwatch(long position, int types); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java index 41841ac5..641dcd89 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java @@ -4,7 +4,6 @@ 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.building.SectionUpdateRouter; 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; @@ -78,7 +77,7 @@ public class RenderService, J extends Vi this.traversal = new HierarchicalOcclusionTraverser(this.nodeManager, this.nodeCleaner); - world.setDirtyCallback(router::forward); + world.setDirtyCallback(router::forwardEvent); Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome); world.getMapper().setBiomeCallback(this.modelService::addBiome); @@ -154,7 +153,7 @@ public class RenderService, J extends Vi private int q = -60; public void setup(Camera camera) { final int W = 32; - final int H = 2; + final int H = 3; boolean SIDED = false; for (int i = 0; i<64 && q<((W*2+1)*(W*2+1)*H)&&q++>=0;i++) { this.nodeManager.insertTopLevelNode(WorldEngine.getWorldSectionId(4, (q%(W*2+1))-(SIDED?0:W), ((q/(W*2+1))/(W*2+1))-1, ((q/(W*2+1))%(W*2+1))-(SIDED?0:W))); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/SectionUpdateRouter.java b/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java similarity index 92% rename from src/main/java/me/cortex/voxy/client/core/rendering/building/SectionUpdateRouter.java rename to src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java index 71e5168a..a613d7ec 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/SectionUpdateRouter.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java @@ -1,8 +1,6 @@ -package me.cortex.voxy.client.core.rendering.building; +package me.cortex.voxy.client.core.rendering; import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldSection; @@ -10,7 +8,7 @@ import java.util.function.LongConsumer; import static me.cortex.voxy.common.world.WorldEngine.UPDATE_TYPE_BLOCK_BIT; -public class SectionUpdateRouter { +public class SectionUpdateRouter implements ISectionWatcher { private static final int SLICES = 1<<3; public interface IChildUpdate {void accept(WorldSection section);} @@ -78,7 +76,7 @@ public class SectionUpdateRouter { } } - public void forward(WorldSection section, int type) { + public void forwardEvent(WorldSection section, int type) { final long position = section.key; var set = this.slices[getSliceIndex(position)]; byte types = 0; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java index 8ca63ee9..863ccab6 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java @@ -3,16 +3,20 @@ package me.cortex.voxy.client.core.rendering.hierachical; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +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.building.SectionUpdateRouter; import me.cortex.voxy.client.core.rendering.section.AbstractSectionGeometryManager; import me.cortex.voxy.client.core.rendering.util.UploadStream; import me.cortex.voxy.client.core.util.ExpandingObjectAllocationList; import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.commonImpl.VoxyCommon; import net.caffeinemc.mods.sodium.client.util.MathUtil; +import org.lwjgl.system.MemoryUtil; import java.util.List; @@ -26,7 +30,7 @@ import static me.cortex.voxy.common.world.WorldEngine.MAX_LOD_LAYERS; public class NodeManager { - private static final boolean VERIFY_NODE_MANAGER_OPERATIONS = VoxyCommon.isVerificationFlagOn("nodeManager"); + private static final boolean VERIFY_NODE_MANAGER_OPERATIONS = true;//VoxyCommon.isVerificationFlagOn("nodeManager"); //Assumptions: // all nodes have children (i.e. all nodes have at least one child existence bit set at all times) // leaf nodes always contain geometry (empty geometry counts as geometry (it just doesnt take any memory to store)) @@ -50,6 +54,8 @@ public class NodeManager { public static final int NULL_GEOMETRY_ID = -1; public static final int EMPTY_GEOMETRY_ID = -2; + public static final int NULL_REQUEST_ID = NodeStore.REQUEST_ID_MSK; + public static final int SENTINEL_EMPTY_CHILD_PTR = NodeStore.NODE_ID_MSK-1; public static final int NODE_ID_MSK = ((1<<24)-1); private static final int NODE_TYPE_MSK = 0b11<<30; @@ -66,11 +72,12 @@ public class NodeManager { private final ExpandingObjectAllocationList childRequests = new ExpandingObjectAllocationList<>(NodeChildRequest[]::new); private final IntOpenHashSet nodeUpdates = new IntOpenHashSet(); private final AbstractSectionGeometryManager geometryManager; - private final SectionUpdateRouter updateRouter; + private final ISectionWatcher watcher; private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap(); private final NodeStore nodeData; public final int maxNodeCount; private final IntArrayList topLevelNodeIds = new IntArrayList(); + private final LongOpenHashSet topLevelNodes = new LongOpenHashSet(); private int activeNodeRequestCount; public interface ClearIdCallback {void clearId(int id);} @@ -78,7 +85,7 @@ public class NodeManager { public void setClearIdCallback(ClearIdCallback callback) {this.clearIdCallback = callback;} private void clearId(int id) { if (this.clearIdCallback != null) this.clearIdCallback.clearId(id); } - public NodeManager(int maxNodeCount, AbstractSectionGeometryManager geometryManager, SectionUpdateRouter updateRouter) { + public NodeManager(int maxNodeCount, AbstractSectionGeometryManager geometryManager, ISectionWatcher watcher) { if (!MathUtil.isPowerOfTwo(maxNodeCount)) { throw new IllegalArgumentException("Max node count must be a power of 2"); } @@ -86,7 +93,7 @@ public class NodeManager { throw new IllegalArgumentException("Max node count cannot exceed 2^24"); } this.activeSectionMap.defaultReturnValue(-1); - this.updateRouter = updateRouter; + this.watcher = watcher; this.maxNodeCount = maxNodeCount; this.nodeData = new NodeStore(maxNodeCount); this.geometryManager = geometryManager; @@ -103,8 +110,9 @@ public class NodeManager { var request = new SingleNodeRequest(pos); int id = this.singleRequests.put(request); - this.updateRouter.watch(pos, WorldEngine.UPDATE_FLAGS); + this.watcher.watch(pos, WorldEngine.UPDATE_FLAGS); this.activeSectionMap.put(pos, id|NODE_TYPE_REQUEST|REQUEST_TYPE_SINGLE); + this.topLevelNodes.add(pos); } public void removeTopLevelNode(long pos) { @@ -119,6 +127,9 @@ public class NodeManager { // OR!! just ensure the list is always ordered?? maybe? idk i think hashmap is best // since the array list might get shuffled as nodes are removed // since need to move the entry at the end of the array to fill a hole made + + // remove from topLevelNodes aswell + } @@ -182,6 +193,8 @@ public class NodeManager { if (this.updateNodeGeometry(nodeId&NODE_ID_MSK, sectionResult) != 0) { this.invalidateNode(nodeId&NODE_ID_MSK); } + } else { + throw new IllegalStateException(); } } @@ -281,7 +294,7 @@ public class NodeManager { throw new IllegalStateException("Child pos was in a request but not in active section map"); } - if (!this.updateRouter.unwatch(cPos, WorldEngine.UPDATE_FLAGS)) { + if (!this.watcher.unwatch(cPos, WorldEngine.UPDATE_FLAGS)) { throw new IllegalStateException("Child pos was not being watched"); } } @@ -299,7 +312,7 @@ public class NodeManager { if (this.activeSectionMap.put(cPos, requestId|NODE_TYPE_REQUEST|REQUEST_TYPE_CHILD) != -1) { throw new IllegalStateException("Child pos was already in active section tracker but was part of a request"); } - if (!this.updateRouter.watch(cPos, WorldEngine.UPDATE_FLAGS)) { + if (!this.watcher.watch(cPos, WorldEngine.UPDATE_FLAGS)) { throw new IllegalStateException("Child pos update router issue"); } } @@ -322,14 +335,14 @@ public class NodeManager { private void updateChildSectionsInner(long pos, int nodeId, byte childExistence) { //Very complex and painful operation - - //TODO: operation of needing to create a request node to add new sections - // (or modify the node to remove a child node (recursively probably ;-;)) - + if (childExistence == 0) { + Logger.warn("Inner node child existence is changing to 0, this is mild bad"); + } //This works in 2 parts, adding and removing, adding is (surprisingly) much easier than removing // adding, either adds to a request, or creates a new request byte existence = this.nodeData.getNodeChildExistence(nodeId); + byte add = (byte) ((existence^childExistence)&childExistence); if (add != 0) {//We have nodes to add if (!this.nodeData.isNodeRequestInFlight(nodeId)) {//If there is not an existing request, create it @@ -356,7 +369,7 @@ public class NodeManager { if (this.activeSectionMap.put(cPos, requestId|NODE_TYPE_REQUEST|REQUEST_TYPE_CHILD) != -1) { throw new IllegalStateException("Child pos was already in active section tracker but was part of a request"); } - if (!this.updateRouter.watch(cPos, WorldEngine.UPDATE_FLAGS)) { + if (!this.watcher.watch(cPos, WorldEngine.UPDATE_FLAGS)) { throw new IllegalStateException("Child pos update router issue"); } } @@ -365,10 +378,10 @@ public class NodeManager { //Update the nodes existence msk to the new one // this needs to be before the removal since that may invoke requestFinish, which expects updated node masks //TODO: verify this - this.nodeData.setNodeChildExistence(nodeId&NODE_ID_MSK, childExistence); + this.nodeData.setNodeChildExistence(nodeId, childExistence); // Do removals - byte rem = (byte) ((existence^childExistence)&existence); + int rem = ((existence^childExistence)&existence)&0xFF; if (rem != 0) { //If there is an inflight request, update it w.r.t removals if (this.nodeData.isNodeRequestInFlight(nodeId)) { @@ -377,7 +390,7 @@ public class NodeManager { if (request.getPosition() != pos) throw new IllegalStateException("Request is not at pos"); - byte reqRem = (byte) (request.getMsk()&rem); + int reqRem =Byte.toUnsignedInt(request.getMsk())&rem; if (reqRem != 0) { //There are things in the request to remove for (int i = 0; i < 8; i++) { @@ -392,37 +405,144 @@ public class NodeManager { 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"); } - if (!this.updateRouter.unwatch(cPos, WorldEngine.UPDATE_FLAGS)) { + if (!this.watcher.unwatch(cPos, WorldEngine.UPDATE_FLAGS)) { throw new IllegalStateException("Child pos was not being watched"); } } } - //TODO: FIXME: This isnt right, as we need to remove node + geometry if it was in a request aswell as a child? - // BUT dont think thats possible? - - rem ^= reqRem; - //If the request is satisfied, submit the result + } + + if (rem != 0) { + //There are child node entries that need removing + // and of course still delete all the old data + + + + //Compact the node data with respect to what has been removed + int oldPtr = this.nodeData.getChildPtr(nodeId); + int oldCount = this.nodeData.getChildPtrCount(nodeId); + if (oldPtr == -1) { + throw new IllegalStateException(); + } + int oldExistence = 0; + for (int i = 0; i < oldCount; i++) { + if (!this.nodeData.nodeExists(i+oldPtr)) throw new IllegalStateException(); + oldExistence |= 1< sections = new Int2ObjectOpenHashMap<>(); + public MemoryGeometryManager(int maxSections, long geometryCapacity) { + super(maxSections, geometryCapacity); + this.allocation = new HierarchicalBitSet(maxSections); + } + + @Override + public int uploadReplaceSection(int oldId, BuiltSection section) { + if (section.isEmpty()) { + throw new IllegalArgumentException(); + } + if (oldId != -1) { + this.removeSection(oldId); + } + int newId = this.allocation.allocateNext(); + var entry = new Entry(section.position, section.geometryBuffer.size); + if (this.sections.put(newId, entry) != null) { + throw new IllegalStateException(); + } + this.memoryInUse += entry.size; + section.free(); + + Logger.info("Creating geometry with id", newId, "and size", entry.size, "at pos", WorldEngine.pprintPos(entry.pos)); + + return newId; + } + + @Override + public void removeSection(int id) { + if (!this.allocation.free(id)) { + throw new IllegalStateException(); + } + var old = this.sections.remove(id); + if (old == null) { + throw new IllegalStateException(); + } + this.memoryInUse -= old.size; + Logger.info("Removing geometry with id", id, "it was at pos", WorldEngine.pprintPos(old.pos)); + } + + @Override + public void downloadAndRemove(int id, Consumer callback) { + throw new IllegalStateException(); + } + + @Override + public long getUsedCapacity() { + return this.memoryInUse; + } + } + + private static class Watcher implements ISectionWatcher { + private final Long2ByteOpenHashMap updateTypes = new Long2ByteOpenHashMap(); + + @Override + public boolean watch(long position, int types) { + byte current = 0; + boolean had = false; + if (this.updateTypes.containsKey(position)) { + current = this.updateTypes.get(position); + had = true; + } + if (had && current == 0) { + throw new IllegalStateException(); + } + this.updateTypes.put(position, (byte) (current | types)); + byte delta = (byte) (types&(~current)); + Logger.info("Watching pos", WorldEngine.pprintPos(position), "with types", getPrettyTypes(types), "was", getPrettyTypes(current)); + return delta!=0;//returns true if new types where set + } + + @Override + public boolean unwatch(long position, int types) { + if (!this.updateTypes.containsKey(position)) { + throw new IllegalStateException("Pos not in map: " + WorldEngine.pprintPos(position)); + } + byte current = this.updateTypes.get(position); + byte newTypes = (byte) (current&(~types)); + if (newTypes == 0) { + this.updateTypes.remove(position); + } else { + this.updateTypes.put(position, newTypes); + } + Logger.info("UnWatching pos", WorldEngine.pprintPos(position), "removing types", getPrettyTypes(types), "was watching", getPrettyTypes(current), "new types", getPrettyTypes(newTypes)); + return newTypes == 0;//Returns true on removal + } + + private static String[] getPrettyTypes(int msk) { + if ((msk&~UPDATE_FLAGS)!=0) { + throw new IllegalStateException(); + } + String[] types = new String[Integer.bitCount(msk)]; + int i = 0; + if ((msk&UPDATE_TYPE_BLOCK_BIT)!=0) { + types[i++] = "BLOCK"; + } + if ((msk&UPDATE_TYPE_CHILD_EXISTENCE_BIT)!=0) { + types[i++] = "CHILD"; + } + return types; + } + } + + private static class TestBase { + public final MemoryGeometryManager geometryManager; + public final NodeManager nodeManager; + public final Watcher watcher; + + public TestBase() { + this.watcher = new Watcher(); + this.geometryManager = new MemoryGeometryManager(1<<20, 1<<30); + this.nodeManager = new NodeManager(1 << 21, this.geometryManager, this.watcher); + } + + public void putTopPos(long pos) { + this.nodeManager.insertTopLevelNode(pos); + } + + public void meshUpdate(long pos, int childExistence, int geometrySize) { + if (childExistence == -1) { + childExistence = 0xFF; + } + if (childExistence>255) { + throw new IllegalArgumentException(); + } + MemoryBuffer buff = null; + if (geometrySize != 0) { + buff = new MemoryBuffer(geometrySize); + } + var builtGeometry = new BuiltSection(pos, (byte) childExistence, -2, buff, null); + this.nodeManager.processGeometryResult(builtGeometry); + } + + public void request(long pos) { + this.nodeManager.processRequest(pos); + } + + public void childUpdate(long pos, int existence) { + if (existence == -1) { + existence = 0xFF; + } + if (existence>255) { + throw new IllegalArgumentException(); + } + this.nodeManager.processChildChange(pos, (byte) existence); + } + + public boolean printNodeChanges() { + var changes = this.nodeManager._generateChangeList(); + if (changes == null) { + return false; + } + for (int c = 0; c < changes.size/20; c++) { + long ptr = changes.address+20L*c; + int nodeId = MemoryUtil.memGetInt(ptr); ptr+=4; + long pos = Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr))<<32; ptr += 4; + pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4; + int z = MemoryUtil.memGetInt(ptr); ptr += 4; + int w = MemoryUtil.memGetInt(ptr); ptr += 4; + + int childPtr = w&0xFFFFFF; + int geometry = z&0xFFFFFF; + short flags = 0; + + flags |= (short) ((z>>>24)&0xFF); + flags |= (short) (((w>>>24)&0xFF)<<8); + + Logger.info("Node update, id:",nodeId,"pos:",WorldEngine.pprintPos(pos),"childPtr:",childPtr,"geometry:",geometry,"flags:",flags); + } + changes.free(); + return true; + } + + public void removeNodeGeometry(long pos) { + this.nodeManager.removeNodeGeometry(pos); + } + + public void verifyIntegrity() { + this.nodeManager.verifyIntegrity(this.watcher.updateTypes.keySet()); + } + } + + private static void fillInALl(TestBase test, long pos, Long2IntFunction converter) { + test.request(pos); + + + int ce = converter.get(pos); + //Satisfy request for all the children + for (int i = 0; i<8;i++) { + if ((ce&(1<> seenTraces = new HashSet<>(); + + Logger.SHUTUP = true; + + if (false) { + for (int q = 0; q < ITER_COUNT; q++) { + //Logger.info("Iteration "+ q); + if (runTest(INNER_ITER_COUNT, q, seenTraces)) { + finished.incrementAndGet(); + } + } + } else { + IntStream.range(0, ITER_COUNT).parallel().forEach(i->{ + if (runTest(INNER_ITER_COUNT, i, seenTraces)) { + finished.incrementAndGet(); + } + }); + } + System.out.println("Finished " + finished.get() + " iterations out of " + ITER_COUNT); + } + private static long rPos(Random r) { + int lvl = r.nextInt(5); + if (lvl==4) { + return WorldEngine.getWorldSectionId(4,0,0,0); + } + int bound = 16>>lvl; + return WorldEngine.getWorldSectionId(lvl, r.nextInt(bound), r.nextInt(bound), r.nextInt(bound)); + } + + private static boolean runTest(int ITERS, int testIdx, Set> traces) { + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + + Random r = new Random(testIdx * 1234L); + try { + var test = new TestBase(); + //Fuzzy bruteforce everything + test.putTopPos(POS_A); + for (int i = 0; i < ITERS; i++) { + long pos = rPos(r); + int op = r.nextInt(3); + int extra = r.nextInt(256); + boolean hasGeometry = r.nextBoolean(); + if (op == 0) { + test.request(pos); + } + if (op == 1) { + test.childUpdate(pos, extra); + } + if (op == 2) { + test.meshUpdate(pos, extra, hasGeometry ? 100 : 0); + } + test.printNodeChanges(); + test.verifyIntegrity(); + } + test.childUpdate(POS_A, 0); + test.meshUpdate(POS_A, 0, 0); + if (test.geometryManager.memoryInUse != 0) { + throw new IllegalStateException(); + } + return true; + } catch (Exception e) { + var trace = new ArrayList<>(List.of(e.getStackTrace())); + while (!trace.getLast().getMethodName().equals("runTest")) trace.removeLast();//Very hacky budget filter + synchronized (traces) { + if (traces.add(trace)) { + e.printStackTrace(); + } + } + return false; + } + } + + + + + + + + public static void main3(String[] args) { + Logger.INSERT_CLASS = false; + + if (false) { + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + test.meshUpdate(POS_A, 3, 0); + test.printNodeChanges(); + test.request(POS_A); + test.meshUpdate(makeChildPos(POS_A, 0), -1, 100); + test.meshUpdate(makeChildPos(POS_A, 1), -1, 100); + test.printNodeChanges(); + Logger.info("TEST: Created full node"); + test.childUpdate(POS_A, 0); + test.printNodeChanges(); + Logger.info("BB"); + test.meshUpdate(POS_A, 0, 0); + test.printNodeChanges(); + } + + if (false) { + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + test.meshUpdate(POS_A, 1, 0); + test.request(POS_A); + test.meshUpdate(makeChildPos(POS_A, 0), -1, 100); + test.childUpdate(POS_A, 0); + } + + + if (false) {//Crash E + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + test.meshUpdate(POS_A, 3, 0); + test.printNodeChanges(); + test.request(POS_A); + test.meshUpdate(makeChildPos(POS_A, 0), -1, 100); + test.meshUpdate(makeChildPos(POS_A, 1), -1, 100); + test.printNodeChanges(); + Logger.info("TEST: Created full node"); + test.childUpdate(POS_A, 0b110); + test.printNodeChanges(); + test.childUpdate(POS_A, 0b10); + test.printNodeChanges(); + } + + if (false) {//Crash D + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + test.meshUpdate(POS_A, 3, 0); + test.printNodeChanges(); + test.request(POS_A); + test.meshUpdate(makeChildPos(POS_A, 0), -1, 100); + test.meshUpdate(makeChildPos(POS_A, 1), -1, 100); + test.printNodeChanges(); + Logger.info("TEST: Created full node"); + test.childUpdate(POS_A, 0b110); + test.printNodeChanges(); + test.meshUpdate(makeChildPos(POS_A, 2), -1, 100); + test.printNodeChanges(); + } + + if (false) {//Crash c + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + test.meshUpdate(POS_A, 3, 0); + test.printNodeChanges(); + test.request(POS_A); + test.meshUpdate(makeChildPos(POS_A, 0), -1, 100); + test.meshUpdate(makeChildPos(POS_A, 1), -1, 100); + test.printNodeChanges(); + Logger.info("TEST: Created full node"); + test.childUpdate(POS_A, 0b1111); + test.printNodeChanges(); + test.meshUpdate(makeChildPos(POS_A, 2), -1, 100); + test.printNodeChanges(); + Logger.info("TEST: Executing funny"); + test.childUpdate(POS_A, 0b0110); + test.printNodeChanges(); + } + + if (false) {//Crash b + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + test.meshUpdate(POS_A, 3, 0); + test.printNodeChanges(); + test.request(POS_A); + test.meshUpdate(makeChildPos(POS_A, 0), -1, 100); + test.meshUpdate(makeChildPos(POS_A, 1), -1, 100); + test.printNodeChanges(); + Logger.info("TEST: Created full node"); + test.childUpdate(POS_A, 0b1111); + test.printNodeChanges(); + test.meshUpdate(makeChildPos(POS_A, 2), -1, 100); + test.printNodeChanges(); + test.childUpdate(POS_A, 0b1110); + test.printNodeChanges(); + test.childUpdate(POS_A, 0b0110); + test.printNodeChanges(); + } + + if (false) {// Test case A, known crash + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + test.meshUpdate(POS_A, 7, 0); + test.printNodeChanges(); + test.request(POS_A); + test.meshUpdate(makeChildPos(POS_A, 0), -1, 100); + test.meshUpdate(makeChildPos(POS_A, 1), -1, 100); + test.meshUpdate(makeChildPos(POS_A, 2), -1, 100); + test.printNodeChanges(); + Logger.error("CHANGING CHILD A"); + test.childUpdate(POS_A, 1); + test.printNodeChanges(); + Logger.error("CHANGING CHILD B"); + test.childUpdate(POS_A, 3); + test.printNodeChanges(); + test.meshUpdate(makeChildPos(POS_A, 1), -1, 100); + test.printNodeChanges(); + } + + } + + + public static void main2(String[] args) { + Logger.INSERT_CLASS = false; + + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + + test.meshUpdate(POS_A, -1, 0); + fillInALl(test, POS_A, a->-1); + + test.printNodeChanges(); + Logger.info("\n\n"); + + test.removeNodeGeometry(WorldEngine.getWorldSectionId(0,0,0,0)); + test.printNodeChanges(); + test.removeNodeGeometry(WorldEngine.getWorldSectionId(3,0,0,0)); + test.printNodeChanges(); + Logger.info("changing child existance"); + test.childUpdate(WorldEngine.getWorldSectionId(4,0,0,0), 1); + test.childUpdate(WorldEngine.getWorldSectionId(3,0,0,0), 1); + test.childUpdate(WorldEngine.getWorldSectionId(2,0,0,0), 1); + test.childUpdate(WorldEngine.getWorldSectionId(1,0,0,0), 1); + test.printNodeChanges(); + } + + public static void main1(String[] args) { + Logger.INSERT_CLASS = false; + + Random r = new Random(1234); + Long2IntOpenHashMap aa = new Long2IntOpenHashMap(); + Long2IntFunction cc = p-> aa.computeIfAbsent(p, poss->{int b = r.nextInt()&0xFF; + while (b==0) b = r.nextInt()&0xFF; + return b;}); + + var test = new TestBase(); + long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0); + test.putTopPos(POS_A); + + test.meshUpdate(POS_A, cc.get(POS_A), 0); + fillInALl(test, POS_A, cc); + + test.printNodeChanges(); + Logger.info("\n\n"); + + var positions = new ArrayList<>(aa.keySet().stream().filter(k->{ + return WorldEngine.getLevel(k)!=0; + }).toList()); + positions.sort(Long::compareTo); + Collections.shuffle(positions, r); + + Logger.info("Removing", WorldEngine.pprintPos(positions.get(0))); + test.removeNodeGeometry(positions.get(0)); + test.printNodeChanges(); + } + + + private static int getChildIdx(long pos) { + int x = WorldEngine.getX(pos); + int y = WorldEngine.getY(pos); + int z = WorldEngine.getZ(pos); + return (x&1)|((y&1)<<2)|((z&1)<<1); + } + + private static long makeChildPos(long basePos, int addin) { + int lvl = WorldEngine.getLevel(basePos); + if (lvl == 0) { + throw new IllegalArgumentException("Cannot create a child lower than lod level 0"); + } + return WorldEngine.getWorldSectionId(lvl-1, + (WorldEngine.getX(basePos)<<1)|(addin&1), + (WorldEngine.getY(basePos)<<1)|((addin>>2)&1), + (WorldEngine.getZ(basePos)<<1)|((addin>>1)&1)); + } + + private long makeParentPos(long pos) { + int lvl = WorldEngine.getLevel(pos); + if (lvl == MAX_LOD_LAYERS-1) { + throw new IllegalArgumentException("Cannot create a parent higher than LoD " + (MAX_LOD_LAYERS-1)); + } + return WorldEngine.getWorldSectionId(lvl+1, + WorldEngine.getX(pos)>>1, + WorldEngine.getY(pos)>>1, + WorldEngine.getZ(pos)>>1); + } + +} diff --git a/src/main/java/me/cortex/voxy/common/Logger.java b/src/main/java/me/cortex/voxy/common/Logger.java index 9029f7fd..84a1bcc0 100644 --- a/src/main/java/me/cortex/voxy/common/Logger.java +++ b/src/main/java/me/cortex/voxy/common/Logger.java @@ -5,13 +5,19 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.stream.Collectors; import java.util.stream.Stream; public class Logger { + public static boolean INSERT_CLASS = true; + public static boolean SHUTUP = false; private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger("Voxy"); public static void error(Object... args) { + if (SHUTUP) { + return; + } Throwable throwable = null; for (var i : args) { if (i instanceof Throwable) { @@ -19,14 +25,17 @@ public class Logger { } } var stackEntry = new Throwable().getStackTrace()[1]; - String error = "["+stackEntry.getClassName()+"]: "+ Stream.of(args).map(Object::toString).collect(Collectors.joining(" ")); + String error = (INSERT_CLASS?("["+stackEntry.getClassName()+"]: "):"") + Stream.of(args).map(Logger::objToString).collect(Collectors.joining(" ")); LOGGER.error(error, throwable); - if (!VoxyCommon.IS_DEDICATED_SERVER) { + if (VoxyCommon.IS_IN_MINECRAFT && !VoxyCommon.IS_DEDICATED_SERVER) { MinecraftClient.getInstance().executeSync(()->{var player = MinecraftClient.getInstance().player; if (player != null) player.sendMessage(Text.literal(error), true);}); } } public static void warn(Object... args) { + if (SHUTUP) { + return; + } Throwable throwable = null; for (var i : args) { if (i instanceof Throwable) { @@ -34,10 +43,13 @@ public class Logger { } } var stackEntry = new Throwable().getStackTrace()[1]; - LOGGER.warn("["+stackEntry.getClassName()+"]: "+ Stream.of(args).map(Object::toString).collect(Collectors.joining(" ")), throwable); + LOGGER.warn((INSERT_CLASS?("["+stackEntry.getClassName()+"]: "):"") + Stream.of(args).map(Logger::objToString).collect(Collectors.joining(" ")), throwable); } public static void info(Object... args) { + if (SHUTUP) { + return; + } Throwable throwable = null; for (var i : args) { if (i instanceof Throwable) { @@ -45,6 +57,16 @@ public class Logger { } } var stackEntry = new Throwable().getStackTrace()[1]; - LOGGER.info("["+stackEntry.getClassName()+"]: "+ Stream.of(args).map(Object::toString).collect(Collectors.joining(" ")), throwable); + LOGGER.info((INSERT_CLASS?("["+stackEntry.getClassName()+"]: "):"") + Stream.of(args).map(Logger::objToString).collect(Collectors.joining(" ")), throwable); + } + + private static String objToString(Object obj) { + if (obj == null) { + return "NULL"; + } + if (obj.getClass().isArray()) { + return Arrays.deepToString((Object[]) obj); + } + return obj.toString(); } } diff --git a/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java b/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java index 6f8a13be..19f4ccc8 100644 --- a/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java +++ b/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java @@ -105,7 +105,7 @@ public class ActiveSectionTracker { return section; } else { while ((section = holder.obj) == null) - Thread.onSpinWait(); + Thread.yield(); synchronized (cache) { if (section.tryAcquire()) { diff --git a/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java b/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java index fbc72187..be65a65d 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java +++ b/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java @@ -1,5 +1,6 @@ package me.cortex.voxy.commonImpl; +import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.config.Serialization; import net.fabricmc.api.EnvType; import net.fabricmc.api.ModInitializer; @@ -9,14 +10,17 @@ import net.fabricmc.loader.api.ModContainer; public class VoxyCommon implements ModInitializer { public static final String MOD_VERSION; public static final boolean IS_DEDICATED_SERVER; + public static final boolean IS_IN_MINECRAFT; static { ModContainer mod = (ModContainer) FabricLoader.getInstance().getModContainer("voxy").orElse(null); if (mod == null) { - System.err.println("RUNNING WITHOUT MOD"); + IS_IN_MINECRAFT = false; + Logger.error("Running voxy without minecraft"); MOD_VERSION = ""; IS_DEDICATED_SERVER = false; } else { + IS_IN_MINECRAFT = true; var version = mod.getMetadata().getVersion().getFriendlyString(); var commit = mod.getMetadata().getCustomValue("commit").getAsString(); MOD_VERSION = version + "-" + commit;