More work on node manager
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user