More work on node manager

This commit is contained in:
mcrcortex
2024-08-13 09:18:56 +10:00
parent 1d1e244f03
commit d4714989b4
5 changed files with 166 additions and 52 deletions

View File

@@ -50,7 +50,6 @@ public class VoxyConfig {
return config; return config;
} }
public void save() { public void save() {
//Unsafe, todo: fixme! needs to be atomic!
try { try {
Files.writeString(getConfigPath(), GSON.toJson(this)); Files.writeString(getConfigPath(), GSON.toJson(this));
} catch (IOException e) { } catch (IOException e) {

View File

@@ -68,11 +68,11 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome); Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome);
world.getMapper().setBiomeCallback(this.modelService::addBiome); world.getMapper().setBiomeCallback(this.modelService::addBiome);
final int H_WIDTH = 1;
for (int x = -10; x <= 10; x++) { for (int x = -H_WIDTH; x <= H_WIDTH; x++) {
for (int y = -3; y <= 3; y++) { for (int y = -1; y <= 0; y++) {
for (int z = -10; z <= 10; z++) { for (int z = -H_WIDTH; z <= H_WIDTH; z++) {
positionFilterForwarder.watch(0, x, y ,z); this.nodeManager.insertTopLevelNode(WorldEngine.getWorldSectionId(4, x, y, z));
} }
} }
} }

View File

@@ -15,13 +15,26 @@ import org.lwjgl.system.MemoryUtil;
//Contains no logic to interface with the gpu, nor does it contain any gpu buffers //Contains no logic to interface with the gpu, nor does it contain any gpu buffers
public class HierarchicalNodeManager { public class HierarchicalNodeManager {
public static final int NODE_MSK = ((1<<24)-1); public static final int NODE_MSK = ((1<<24)-1);
private static final int NO_NODE = -1;
private static final int SENTINAL_TOP_NODE_INFLIGHT = -2;
private static final int ID_TYPE_MSK = (3<<30);
private static final int ID_TYPE_NONE = 0;
private static final int ID_TYPE_LEAF = (2<<30);
private static final int ID_TYPE_TOP = (1<<30);
public final int maxNodeCount; public final int maxNodeCount;
private final NodeStore nodeData;
private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap();
private final IntOpenHashSet nodeUpdates = new IntOpenHashSet(); private final IntOpenHashSet nodeUpdates = new IntOpenHashSet();
private final ExpandingObjectAllocationList<LeafExpansionRequest> leafRequests = new ExpandingObjectAllocationList<>(LeafExpansionRequest[]::new); private final NodeStore nodeData;
//Map from position->id, the top 2 bits contains specifies the type of id
private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap();
private final ExpandingObjectAllocationList<NodeChildRequest> requests = new ExpandingObjectAllocationList<>(NodeChildRequest[]::new);
private final AbstractSectionGeometryManager geometryManager; private final AbstractSectionGeometryManager geometryManager;
private final SectionPositionUpdateFilterer updateFilterer; private final SectionPositionUpdateFilterer updateFilterer;
public HierarchicalNodeManager(int maxNodeCount, AbstractSectionGeometryManager geometryManager, SectionPositionUpdateFilterer updateFilterer) { public HierarchicalNodeManager(int maxNodeCount, AbstractSectionGeometryManager geometryManager, SectionPositionUpdateFilterer updateFilterer) {
if (!MathUtil.isPowerOfTwo(maxNodeCount)) { if (!MathUtil.isPowerOfTwo(maxNodeCount)) {
throw new IllegalArgumentException("Max node count must be a power of 2"); throw new IllegalArgumentException("Max node count must be a power of 2");
@@ -29,14 +42,28 @@ public class HierarchicalNodeManager {
if (maxNodeCount>(1<<24)) { if (maxNodeCount>(1<<24)) {
throw new IllegalArgumentException("Max node count cannot exceed 2^24"); throw new IllegalArgumentException("Max node count cannot exceed 2^24");
} }
this.activeSectionMap.defaultReturnValue(-1); this.activeSectionMap.defaultReturnValue(NO_NODE);
this.updateFilterer = updateFilterer; this.updateFilterer = updateFilterer;
this.maxNodeCount = maxNodeCount; this.maxNodeCount = maxNodeCount;
this.nodeData = new NodeStore(maxNodeCount); this.nodeData = new NodeStore(maxNodeCount);
this.geometryManager = geometryManager; this.geometryManager = geometryManager;
} }
public void insertTopLevelNode(long position) {
if (this.activeSectionMap.containsKey(position)) {
throw new IllegalArgumentException("Position already in node set: " + WorldEngine.pprintPos(position));
}
this.activeSectionMap.put(position, SENTINAL_TOP_NODE_INFLIGHT);
this.updateFilterer.watch(position);
}
public void removeTopLevelNode(long position) {
if (!this.activeSectionMap.containsKey(position)) {
throw new IllegalArgumentException("Position not in node set: " + WorldEngine.pprintPos(position));
}
}
public void processRequestQueue(int count, long ptr) { public void processRequestQueue(int count, long ptr) {
for (int requestIndex = 0; requestIndex < count; requestIndex++) { for (int requestIndex = 0; requestIndex < count; requestIndex++) {
@@ -50,7 +77,7 @@ public class HierarchicalNodeManager {
if (!this.nodeData.nodeExists(node)) { if (!this.nodeData.nodeExists(node)) {
throw new IllegalStateException("Tried processing a node that doesnt exist: " + node); throw new IllegalStateException("Tried processing a node that doesnt exist: " + node);
} }
if (this.nodeData.nodeRequestInFlight(node)) { if (this.nodeData.isNodeRequestInFlight(node)) {
throw new IllegalStateException("Tried processing a node that already has a request in flight: " + node + " pos: " + WorldEngine.pprintPos(this.nodeData.nodePosition(node))); throw new IllegalStateException("Tried processing a node that already has a request in flight: " + node + " pos: " + WorldEngine.pprintPos(this.nodeData.nodePosition(node)));
} }
this.nodeData.markRequestInFlight(node); this.nodeData.markRequestInFlight(node);
@@ -59,7 +86,7 @@ public class HierarchicalNodeManager {
//2 branches, either its a leaf node -> emit a leaf request //2 branches, either its a leaf node -> emit a leaf request
// or the nodes geometry must be empty (i.e. culled from the graph/tree) so add to tracker and watch // or the nodes geometry must be empty (i.e. culled from the graph/tree) so add to tracker and watch
if (this.nodeData.isLeafNode(node)) { if (this.nodeData.isLeafNode(node)) {
this.makeLeafRequest(node); this.makeLeafRequest(node, this.nodeData.getNodeChildExistence(node));
} else { } else {
//Verify that the node section is not in the section store. if it is then it is a state desynchonization //Verify that the node section is not in the section store. if it is then it is a state desynchonization
// Note that a section can be "empty" but some of its children might not be // Note that a section can be "empty" but some of its children might not be
@@ -68,26 +95,31 @@ public class HierarchicalNodeManager {
private void makeLeafRequest(int node, byte childExistence) { private void makeLeafRequest(int node, byte childExistence) {
long pos = this.nodeData.nodePosition(node); long pos = this.nodeData.nodePosition(node);
//TODO: the localNodeData should have a bitset of what children are definitely empty
// use that to msk the request, HOWEVER there is a race condition e.g.
// leaf node is requested and has only 1 child marked as non empty
// however then an update occures and a different child now becomes non empty,
// this will trigger a processBuildResult for parent
// so need to ensure that when that happens, if the parent has an inflight leaf expansion request
// for the leaf request to be updated to account for the new maybe child node
// NOTE: a section can have empty geometry but some of its children might not, so need to mark and
// submit a node at that level but with empty section, (specially marked) so that the traversal
// can recurse into those children as needed
//Enqueue a leaf expansion request //Enqueue a leaf expansion request
var request = new LeafExpansionRequest(pos); var request = new NodeChildRequest(pos);
int requestId = this.leafRequests.put(request); int requestId = this.requests.put(request);
//Only request against the childExistence mask, since the guarantee is that if childExistence bit is not set then that child is guaranteed to be empty
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
if ((childExistence&(1<<i))==0) {
//Dont watch or enqueue the child node cause it doesnt exist
continue;
}
long childPos = makeChildPos(pos, i); long childPos = makeChildPos(pos, i);
request.addChildRequirement(i);
//Insert all the children into the tracking map with the node id //Insert all the children into the tracking map with the node id
this.activeSectionMap.put(childPos, 0); if (this.activeSectionMap.put(childPos, requestId|ID_TYPE_LEAF) != NO_NODE) {
throw new IllegalStateException("Leaf request creation failed to insert child into map as a mapping already existed for the node!");
}
//Watch and request the child node at the given position
if (!this.updateFilterer.watch(childPos)) {
throw new IllegalStateException("Failed to watch childPos");
}
} }
this.nodeData.setNodeRequest(node, requestId);
} }
@@ -100,35 +132,66 @@ public class HierarchicalNodeManager {
// when mesh result, need to remove the old child allocation block and make a new block to fit the // when mesh result, need to remove the old child allocation block and make a new block to fit the
// new count of children // new count of children
final long position = update.position();
int nodeId = this.activeSectionMap.get(update.position()); final var geometryData = update.geometry();
if (nodeId == -1) { int nodeId = this.activeSectionMap.get(position);
if (nodeId == NO_NODE) {
System.err.println("Received update for section " + WorldEngine.pprintPos(position) + " however section position not in active in map! discarding");
//Not tracked or mapped to a node!, discard it, it was probably in progress when it was removed from the map //Not tracked or mapped to a node!, discard it, it was probably in progress when it was removed from the map
if (update.geometry() != null) { if (geometryData != null) {
update.geometry().free(); geometryData.free();
} }
} else { } else {
//Part of a request (top bit is set to 1) if (nodeId == SENTINAL_TOP_NODE_INFLIGHT) {
if ((nodeId&(1<<31))!=0) { //Special state for top level nodes that are in flight
nodeId &= ~(1<<31); if (geometryData == null) {
var request = this.leafRequests.get(nodeId); //FIXME: this is a bug, as the child existence could change and have an update sent, resulting in a desync
System.err.println("Top level inflight node " + WorldEngine.pprintPos(position) + " got a child msk update but was still in flight! discarding update");
return;
}
//Allocate a new node id
nodeId = this.nodeData.allocate();
this.activeSectionMap.put(position, nodeId|ID_TYPE_TOP);
int geometry = -1;
if (!geometryData.isEmpty()) {
geometry = this.geometryManager.uploadSection(geometryData);
} else {
geometryData.free();
}
this.fillNode(nodeId, position, geometry, update.childExistence());
} else { } else {
//Not part of a request, just a node update, if node is currently a leaf node, it might have a int type = (nodeId & ID_TYPE_MSK);
// leaf request associated with it, which might need an update if nodeId &= ~ID_TYPE_MSK;
if (type == ID_TYPE_LEAF) {
this.leafDataUpdate(nodeId, update);
} else if (type == ID_TYPE_NONE || type == ID_TYPE_TOP) {
//Not part of a request, just a node update
} else {
throw new IllegalStateException("Should not reach here");
}
} }
} }
} }
private void fillNode(int node, long position, int geometry, byte childExistence) {
this.nodeData.setNodePosition(node, position);
this.nodeData.setNodeGeometry(node, geometry);
this.nodeData.setNodeChildExistence(node, childExistence);
}
private void leafDataUpdate(int nodeId, SectionUpdate update) {
var request = this.requests.get(nodeId);
}
private int updateNodeGeometry(int node, BuiltSection geometry) { private int updateNodeGeometry(int node, BuiltSection geometry) {
int previousGeometry = -1; int previousGeometry = this.nodeData.getNodeGeometry(node);
int newGeometry = -1; int newGeometry = -1;
if (this.nodeData.hasGeometry(node)) { if (previousGeometry != -1) {
previousGeometry = this.nodeData.getNodeGeometry(node);
if (!geometry.isEmpty()) { if (!geometry.isEmpty()) {
newGeometry = this.geometryManager.uploadReplaceSection(previousGeometry, geometry); newGeometry = this.geometryManager.uploadReplaceSection(previousGeometry, geometry);
} else { } else {
@@ -153,6 +216,17 @@ public class HierarchicalNodeManager {
} }
} }
private void createSingleNode() {
}
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)<<1)|((z&1)<<2);
}
private static long makeChildPos(long basePos, int addin) { private static long makeChildPos(long basePos, int addin) {
int lvl = WorldEngine.getLevel(basePos); int lvl = WorldEngine.getLevel(basePos);
if (lvl == 0) { if (lvl == 0) {

View File

@@ -1,7 +1,6 @@
package me.cortex.voxy.client.core.rendering.hierachical2; package me.cortex.voxy.client.core.rendering.hierachical2;
//Request of the leaf node to expand class NodeChildRequest {
class LeafExpansionRequest {
//Child states contain micrometadata in the top bits //Child states contain micrometadata in the top bits
// such as isEmpty, and isEmptyButEventuallyHasNonEmptyChild // such as isEmpty, and isEmptyButEventuallyHasNonEmptyChild
private final long nodePos; private final long nodePos;
@@ -11,13 +10,20 @@ class LeafExpansionRequest {
private byte results; private byte results;
private byte mask; private byte mask;
LeafExpansionRequest(long nodePos) { NodeChildRequest(long nodePos) {
this.nodePos = nodePos; this.nodePos = nodePos;
} }
public int getChildMeshResult(int childIdx) {
if ((this.mask&(1<<childIdx))==0) {
throw new IllegalStateException("Tried getting mesh result of child not in mask");
}
return this.childStates[childIdx];
}
public int putChildResult(int childIdx, int mesh) { public int putChildResult(int childIdx, int mesh) {
if ((this.mask&(1<<childIdx))==0) { if ((this.mask&(1<<childIdx))==0) {
throw new IllegalStateException("Tried putting child into leaf which doesnt match mask"); throw new IllegalStateException("Tried putting child into request which isnt in mask");
} }
//Note the mesh can be -ve meaning empty mesh, but we should still mark that node as having a result //Note the mesh can be -ve meaning empty mesh, but we should still mark that node as having a result
boolean isFirstInsert = (this.results&(1<<childIdx))==0; boolean isFirstInsert = (this.results&(1<<childIdx))==0;

View File

@@ -13,12 +13,17 @@ public final class NodeStore {
this.allocationSet = new HierarchicalBitSet(maxNodeCount); this.allocationSet = new HierarchicalBitSet(maxNodeCount);
} }
private static int id2idx(int idx) {
return idx*LONGS_PER_NODE;
}
public int allocate() { public int allocate() {
int id = this.allocationSet.allocateNext(); int id = this.allocationSet.allocateNext();
if (id < 0) { if (id < 0) {
throw new IllegalStateException("Failed to allocate node slot!"); throw new IllegalStateException("Failed to allocate node slot!");
} }
this.ensureSized(id); this.ensureSized(id);
this.clear(id);
return id; return id;
} }
@@ -31,6 +36,9 @@ public final class NodeStore {
throw new IllegalStateException("Failed to allocate " + count + " consecutive nodes!!"); throw new IllegalStateException("Failed to allocate " + count + " consecutive nodes!!");
} }
this.ensureSized(id + (count-1)); this.ensureSized(id + (count-1));
for (int i = 0; i < count; i++) {
this.clear(id + i);
}
return id; return id;
} }
@@ -45,34 +53,49 @@ public final class NodeStore {
} }
} }
private void free(int nodeId) {
if (!this.allocationSet.free(nodeId)) {
throw new IllegalStateException("Node " + nodeId + " was not allocated!");
}
}
private void clear(int nodeId) {
}
public void setNodePosition(int node, long position) {
this.localNodeData[id2idx(node)] = position;
}
public long nodePosition(int nodeId) { public long nodePosition(int nodeId) {
return this.localNodeData[nodeId<<2]; return this.localNodeData[id2idx(nodeId)];
} }
public boolean nodeExists(int nodeId) { public boolean nodeExists(int nodeId) {
return false; return this.allocationSet.isSet(nodeId);
} }
public boolean hasGeometry(int node) {
return false;
}
public int getNodeGeometry(int node) { public int getNodeGeometry(int node) {
return 0; return -1;
} }
public void setNodeGeometry(int node, int geometryId) { public void setNodeGeometry(int node, int geometryId) {
} }
public void setNodeRequest(int node, int requestId) {
}
public void markRequestInFlight(int nodeId) { public void markRequestInFlight(int nodeId) {
} }
public boolean nodeRequestInFlight(int nodeId) { public boolean isNodeRequestInFlight(int nodeId) {
return false; return false;
} }
@@ -82,9 +105,21 @@ public final class NodeStore {
public byte getNodeChildExistence(int nodeId) {return 0;} public byte getNodeChildExistence(int nodeId) {return 0;}
public void setNodeChildExistence(int node, byte existence) {
}
public int getChildPtr(int nodeId) {
return -1;
}
public void setChildPtr(int nodeId, int ptr) {
}
//Writes out a nodes data to the ptr in the compacted/reduced format //Writes out a nodes data to the ptr in the compacted/reduced format
public void writeNode(long ptr, int nodeId) { public void writeNode(long ptr, int nodeId) {
} }
} }