refactor(procgen): propagate structured execution and cleanup results
This commit is contained in:
1 parent
63f265f3f2
commit
cd76980bb8
15 files changed
+1101
-608
No files matched your search
@@ -86,56 +86,6 @@ const OperationParam* operationParam(const OperationSpec* spec, const std::strin
|
||||
return found == spec->params.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
uint64_t nowNanoseconds() {
|
||||
return uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
}
|
||||
|
||||
PointGraphNodeMetric makeMetric(const std::string& id, const PointSet& points, float milliseconds, bool cacheHit,
|
||||
const std::string& backend = "cpu") {
|
||||
PointGraphNodeMetric metric;
|
||||
metric.id = id;
|
||||
metric.outputCount = points.getCount();
|
||||
metric.milliseconds = milliseconds;
|
||||
metric.cacheHit = cacheHit;
|
||||
metric.backend = backend;
|
||||
if (points.empty()) return metric;
|
||||
const auto& first = points.points().front();
|
||||
metric.minX = metric.maxX = first.x;
|
||||
metric.minY = metric.maxY = first.y;
|
||||
metric.minZ = metric.maxZ = first.z;
|
||||
double densitySum = 0.0;
|
||||
for (const auto& point : points.points()) {
|
||||
metric.minX = std::min(metric.minX, point.x);
|
||||
metric.minY = std::min(metric.minY, point.y);
|
||||
metric.minZ = std::min(metric.minZ, point.z);
|
||||
metric.maxX = std::max(metric.maxX, point.x);
|
||||
metric.maxY = std::max(metric.maxY, point.y);
|
||||
metric.maxZ = std::max(metric.maxZ, point.z);
|
||||
densitySum += point.density;
|
||||
}
|
||||
metric.averageDensity = float(densitySum / double(points.getCount()));
|
||||
return metric;
|
||||
}
|
||||
|
||||
uint64_t spatialSampleUpperBound(const SpatialData& spatial, float spacing) {
|
||||
if (!spatial.hasBounds() || spacing <= 0.f) return 0;
|
||||
const auto axisCount = [spacing](float minimum, float maximum) {
|
||||
if (maximum <= minimum) return uint64_t(1);
|
||||
return uint64_t(std::floor(double(maximum - minimum) / double(spacing) + 0.001)) + 1;
|
||||
};
|
||||
const uint64_t x = axisCount(spatial.getMinX(), spatial.getMaxX());
|
||||
const uint64_t y = spatial.getKind() == "surface.heightfield"
|
||||
? uint64_t(1)
|
||||
: axisCount(spatial.getMinY(), spatial.getMaxY());
|
||||
const uint64_t z = axisCount(spatial.getMinZ(), spatial.getMaxZ());
|
||||
const uint64_t maximum = std::numeric_limits<uint64_t>::max();
|
||||
if (x > maximum / y) return maximum;
|
||||
const uint64_t xy = x * y;
|
||||
return xy > maximum / z ? maximum : xy * z;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PointGraph::PointGraph() : pointCompute_(std::make_shared<PointCompute>()) {}
|
||||
@@ -382,7 +332,14 @@ bool PointGraph::setNodeSubgraph(const std::string& id, PointGraph* graph,
|
||||
}
|
||||
|
||||
PointSet* PointGraph::execute(const std::string& outputId) {
|
||||
error_.clear();
|
||||
auto result = executeResult(outputId);
|
||||
error_ = result.ok() ? std::string{} : result.status().describe();
|
||||
if (!result.ok()) return nullptr;
|
||||
return new PointSet(std::move(result).takeValue());
|
||||
}
|
||||
|
||||
Result<PointSet> PointGraph::executeResult(std::string_view outputId) {
|
||||
const std::string output(outputId);
|
||||
metrics_.clear();
|
||||
cacheHitCount_ = 0;
|
||||
evaluatedNodes_ = 0;
|
||||
@@ -390,15 +347,19 @@ PointSet* PointGraph::execute(const std::string& outputId) {
|
||||
computeFallbackReason_.clear();
|
||||
++executionCount_;
|
||||
if (cancelRequested_) {
|
||||
error_ = "execution cancelled";
|
||||
lastCancelled_ = true;
|
||||
return nullptr;
|
||||
return Result<PointSet>::failure(
|
||||
Diagnostic::error(DiagnosticCode::Cancelled, "execution cancelled", output, {}, "procgen.pointGraph"));
|
||||
}
|
||||
compileExecutionPlan(outputId);
|
||||
if (!error_.empty()) return nullptr;
|
||||
auto compiled = compileExecutionPlan(output);
|
||||
if (!compiled.ok()) return Result<PointSet>::failure(compiled.status());
|
||||
std::unordered_map<std::string, int> states;
|
||||
const PointSet* result = evaluate(outputId, states);
|
||||
return result ? new PointSet(*result) : nullptr;
|
||||
auto result = evaluate(output, states);
|
||||
if (!result.ok()) {
|
||||
lastCancelled_ = result.status().code() == StatusCode::Cancelled;
|
||||
return Result<PointSet>::failure(result.status());
|
||||
}
|
||||
return Result<PointSet>::success(result.value().get());
|
||||
}
|
||||
|
||||
void PointGraph::setExecutionNodeBudget(int nodes) { executionNodeBudget_ = std::max(0, nodes); }
|
||||
@@ -433,11 +394,18 @@ void PointGraph::resetCancellation() {
|
||||
bool PointGraph::wasCancelled() const { return lastCancelled_; }
|
||||
|
||||
bool PointGraph::validate() {
|
||||
error_.clear();
|
||||
auto result = validateResult();
|
||||
error_ = result.ok() ? std::string{} : result.status().describe();
|
||||
return result.ok();
|
||||
}
|
||||
|
||||
Result<void> PointGraph::validateResult() const {
|
||||
std::unordered_map<std::string, int> states;
|
||||
for (const auto& id : nodeOrder_)
|
||||
if (!validateNode(id, states)) return false;
|
||||
return true;
|
||||
for (const auto& id : nodeOrder_) {
|
||||
auto result = validateNode(id, states);
|
||||
if (!result.ok()) return result;
|
||||
}
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
std::string PointGraph::getError() const { return error_; }
|
||||
@@ -560,375 +528,6 @@ std::string PointGraph::getOperationParamDefault(const std::string& operation, i
|
||||
: std::string();
|
||||
}
|
||||
|
||||
const PointSet* PointGraph::evaluate(const std::string& id,
|
||||
std::unordered_map<std::string, int>& states) {
|
||||
const auto found = nodes_.find(id);
|
||||
if (found == nodes_.end()) {
|
||||
error_ = "unknown node: " + id;
|
||||
return nullptr;
|
||||
}
|
||||
Node& node = found->second;
|
||||
if (node.cacheValid) {
|
||||
++cacheHitCount_;
|
||||
PointGraphNodeMetric metric = node.cacheMetric;
|
||||
metric.milliseconds = 0.f;
|
||||
metric.cacheHit = true;
|
||||
metrics_.push_back(std::move(metric));
|
||||
return &node.cache;
|
||||
}
|
||||
if (states[id] == 1) {
|
||||
error_ = "cycle at node: " + id;
|
||||
return nullptr;
|
||||
}
|
||||
if (cancelRequested_) {
|
||||
error_ = "execution cancelled at node: " + id;
|
||||
lastCancelled_ = true;
|
||||
return nullptr;
|
||||
}
|
||||
if (node.operation == "transform") {
|
||||
const PointSet* segment = evaluateTransformSegment(id, states);
|
||||
if (segment || !error_.empty()) return segment;
|
||||
}
|
||||
states[id] = 1;
|
||||
const uint64_t started = nowNanoseconds();
|
||||
std::string nodeBackend = "cpu";
|
||||
const PointSet* first = nullptr;
|
||||
const PointSet* second = nullptr;
|
||||
if (!node.inputs[0].empty()) {
|
||||
first = evaluate(node.inputs[0], states);
|
||||
if (!first) return nullptr;
|
||||
}
|
||||
if (!node.inputs[1].empty()) {
|
||||
second = evaluate(node.inputs[1], states);
|
||||
if (!second) return nullptr;
|
||||
}
|
||||
if (executionNodeBudget_ > 0 && evaluatedNodes_ >= executionNodeBudget_) {
|
||||
error_ = "execution node budget exceeded at node: " + id;
|
||||
lastCancelled_ = true;
|
||||
return nullptr;
|
||||
}
|
||||
++evaluatedNodes_;
|
||||
|
||||
if (node.operation == "input") {
|
||||
if (!node.hasPoints) error_ = "input node has no points: " + id;
|
||||
else node.cache = node.points;
|
||||
} else if (node.operation == "spatial.sample") {
|
||||
const float spacing = floatValue(node, "spacing", 1.f);
|
||||
if (!node.spatial) error_ = "spatial.sample has no spatial data: " + id;
|
||||
else if (maxNodeOutputPoints_ > 0 &&
|
||||
spatialSampleUpperBound(*node.spatial, spacing) > uint64_t(maxNodeOutputPoints_))
|
||||
error_ = "spatial.sample exceeds node point budget at node: " + id;
|
||||
else
|
||||
node.cache = node.spatial->sample(spacing, uint32_t(intValue(node, "seed", 1)),
|
||||
floatValue(node, "jitter", 0.f));
|
||||
} else if (node.operation == "spatial.filter") {
|
||||
if (!first || !node.spatial) error_ = "spatial.filter requires input and spatial data: " + id;
|
||||
else node.cache = node.spatial->filter(*first, intValue(node, "invert", 0) != 0);
|
||||
} else if (node.operation == "spatial.project") {
|
||||
if (!first || !node.spatial) error_ = "spatial.project requires input and spatial data: " + id;
|
||||
else node.cache = node.spatial->project(*first);
|
||||
} else if (node.operation == "biome.generate") {
|
||||
const float spacing = floatValue(node, "spacing", 1.f);
|
||||
if (!node.spatial || !node.biomeRules)
|
||||
error_ = "biome.generate requires spatial data and biome rules: " + id;
|
||||
else if (maxNodeOutputPoints_ > 0 &&
|
||||
spatialSampleUpperBound(*node.spatial, spacing) > uint64_t(maxNodeOutputPoints_))
|
||||
error_ = "biome.generate exceeds node point budget at node: " + id;
|
||||
else {
|
||||
std::unique_ptr<PointSet> result(node.biomeRules->generate(
|
||||
node.spatial.get(), spacing, uint32_t(intValue(node, "seed", 1)),
|
||||
floatValue(node, "jitter", 0.f)));
|
||||
if (!result)
|
||||
error_ = "biome.generate failed at " + id + ": " + node.biomeRules->getError();
|
||||
else
|
||||
node.cache = *result;
|
||||
}
|
||||
} else if (node.operation == "grammar.generate") {
|
||||
if (!first || !node.shapeGrammar)
|
||||
error_ = "grammar.generate requires input and shape grammar: " + id;
|
||||
else {
|
||||
std::unique_ptr<PointSet> result(node.shapeGrammar->generate(
|
||||
stringValue(node, "grammar"), const_cast<PointSet*>(first),
|
||||
uint32_t(intValue(node, "seed", 1)),
|
||||
intValue(node, "acceptIncomplete", 1) != 0));
|
||||
if (!result)
|
||||
error_ = "grammar.generate failed at " + id + ": " +
|
||||
node.shapeGrammar->getError();
|
||||
else
|
||||
node.cache = *result;
|
||||
}
|
||||
} else if (node.operation == "merge") {
|
||||
const uint64_t outputCount = first && second
|
||||
? uint64_t(first->getCount()) + uint64_t(second->getCount())
|
||||
: 0;
|
||||
if (!first || !second) error_ = "merge requires two inputs: " + id;
|
||||
else if (maxNodeOutputPoints_ > 0 && outputCount > uint64_t(maxNodeOutputPoints_))
|
||||
error_ = "merge exceeds node point budget at node: " + id;
|
||||
else node.cache = mergePointSets(*first, *second);
|
||||
} else if (node.operation == "copy.points") {
|
||||
const int maxPoints = intValue(node, "maxPoints", 100000);
|
||||
const uint64_t outputCount = first && second
|
||||
? uint64_t(first->getCount()) * uint64_t(second->getCount())
|
||||
: 0;
|
||||
if (!first || !second) error_ = "copy.points requires source and target inputs: " + id;
|
||||
else if (maxPoints < 0 || outputCount > uint64_t(maxPoints))
|
||||
error_ = "copy.points output exceeds maxPoints at node: " + id;
|
||||
else if (maxNodeOutputPoints_ > 0 && outputCount > uint64_t(maxNodeOutputPoints_))
|
||||
error_ = "copy.points exceeds node point budget at node: " + id;
|
||||
else
|
||||
node.cache = copyPointsToTargets(
|
||||
*first, *second, intValue(node, "inheritTargetAttributes", 1) != 0);
|
||||
} else if (node.operation == "density.remap") {
|
||||
const float inputMin = floatValue(node, "inputMin", 0.f);
|
||||
const float inputMax = floatValue(node, "inputMax", 1.f);
|
||||
if (!first) error_ = "density.remap requires input: " + id;
|
||||
else if (inputMin == inputMax)
|
||||
error_ = "density.remap requires a non-zero input range: " + id;
|
||||
else
|
||||
node.cache = remapPointDensity(
|
||||
*first, inputMin, inputMax, floatValue(node, "outputMin", 0.f),
|
||||
floatValue(node, "outputMax", 1.f), intValue(node, "clamp", 1) != 0);
|
||||
} else if (node.operation == "attribute.math.float") {
|
||||
const std::string attribute = stringValue(node, "attribute");
|
||||
std::string outputAttribute = stringValue(node, "outputAttribute");
|
||||
if (outputAttribute.empty()) outputAttribute = attribute;
|
||||
const std::string operation = stringValue(node, "operation", "multiply");
|
||||
const float operand = floatValue(node, "operand", 1.f);
|
||||
const bool operationKnown = operation == "add" || operation == "subtract" ||
|
||||
operation == "multiply" || operation == "divide" ||
|
||||
operation == "min" || operation == "max";
|
||||
if (!first) error_ = "attribute.math.float requires input: " + id;
|
||||
else if (attribute.empty()) error_ = "attribute.math.float requires an attribute: " + id;
|
||||
else if (!operationKnown)
|
||||
error_ = "attribute.math.float has invalid operation at node: " + id;
|
||||
else if (operation == "divide" && operand == 0.f)
|
||||
error_ = "attribute.math.float cannot divide by zero at node: " + id;
|
||||
else
|
||||
node.cache = mathPointFloatAttribute(
|
||||
*first, attribute, outputAttribute, operation, operand,
|
||||
floatValue(node, "defaultValue", 0.f));
|
||||
} else if (node.operation == "transform") {
|
||||
if (!first) error_ = "transform requires input: " + id;
|
||||
else {
|
||||
const bool gpuEligible =
|
||||
!first->empty() &&
|
||||
(computePolicy_ == "gpu" || (computePolicy_ == "auto" && first->getCount() >= computeMinimumPoints_));
|
||||
const bool gpuComplete =
|
||||
gpuEligible &&
|
||||
pointCompute_->transform(*first, node.cache, floatValue(node, "x", 0.f), floatValue(node, "y", 0.f),
|
||||
floatValue(node, "z", 0.f), floatValue(node, "yaw", 0.f),
|
||||
floatValue(node, "scaleX", 1.f), floatValue(node, "scaleY", 1.f),
|
||||
floatValue(node, "scaleZ", 1.f));
|
||||
if (gpuComplete) {
|
||||
#ifdef EVENGINE_WEBGPU
|
||||
nodeBackend = "webgpu";
|
||||
#else
|
||||
nodeBackend = "vulkan";
|
||||
#endif
|
||||
} else {
|
||||
if (gpuEligible) computeFallbackReason_ = pointCompute_->getError();
|
||||
node.cache = transformPointSet(*first, floatValue(node, "x", 0.f), floatValue(node, "y", 0.f),
|
||||
floatValue(node, "z", 0.f), floatValue(node, "yaw", 0.f),
|
||||
floatValue(node, "scaleX", 1.f), floatValue(node, "scaleY", 1.f),
|
||||
floatValue(node, "scaleZ", 1.f));
|
||||
}
|
||||
}
|
||||
} else if (node.operation == "filter.float") {
|
||||
if (!first) error_ = "filter.float requires input: " + id;
|
||||
else
|
||||
node.cache = filterPointFloatAttribute(
|
||||
*first, stringValue(node, "attribute"), floatValue(node, "min", 0.f),
|
||||
floatValue(node, "max", 1.f), intValue(node, "invert", 0) != 0);
|
||||
} else if (node.operation == "filter.string") {
|
||||
if (!first) error_ = "filter.string requires input: " + id;
|
||||
else
|
||||
node.cache = filterPointStringAttribute(
|
||||
*first, stringValue(node, "attribute"), stringValue(node, "value"),
|
||||
intValue(node, "invert", 0) != 0);
|
||||
} else if (node.operation == "filter.slope") {
|
||||
if (!first) error_ = "filter.slope requires input: " + id;
|
||||
else
|
||||
node.cache = filterPointSlope(*first, floatValue(node, "minDegrees", 0.f),
|
||||
floatValue(node, "maxDegrees", 90.f));
|
||||
} else if (node.operation == "attribute.set.float") {
|
||||
if (!first) error_ = "attribute.set.float requires input: " + id;
|
||||
else {
|
||||
node.cache = *first;
|
||||
const std::string attribute = stringValue(node, "attribute");
|
||||
if (attribute.empty()) error_ = "attribute.set.float requires attribute: " + id;
|
||||
else
|
||||
for (int i = 0; i < node.cache.getCount(); ++i)
|
||||
node.cache.setFloatAttribute(i, attribute, floatValue(node, "value", 0.f));
|
||||
}
|
||||
} else if (node.operation == "attribute.set.string") {
|
||||
if (!first) error_ = "attribute.set.string requires input: " + id;
|
||||
else {
|
||||
node.cache = *first;
|
||||
const std::string attribute = stringValue(node, "attribute");
|
||||
if (attribute.empty()) error_ = "attribute.set.string requires attribute: " + id;
|
||||
else
|
||||
for (int i = 0; i < node.cache.getCount(); ++i)
|
||||
node.cache.setStringAttribute(i, attribute, stringValue(node, "value"));
|
||||
}
|
||||
} else if (node.operation == "density.cull") {
|
||||
if (!first) error_ = "density.cull requires input: " + id;
|
||||
else
|
||||
node.cache = densityCullPoints(*first, uint32_t(intValue(node, "seed", 1)),
|
||||
floatValue(node, "multiplier", 1.f));
|
||||
} else if (node.operation == "self.prune") {
|
||||
if (!first) error_ = "self.prune requires input: " + id;
|
||||
else node.cache = selfPrunePoints(*first, floatValue(node, "radius", 1.f));
|
||||
} else if (node.operation == "jitter") {
|
||||
if (!first) error_ = "jitter requires input: " + id;
|
||||
else
|
||||
node.cache = jitterPointPositions(*first, uint32_t(intValue(node, "seed", 1)),
|
||||
floatValue(node, "x", 0.f),
|
||||
floatValue(node, "z", 0.f));
|
||||
} else if (node.operation == "branch") {
|
||||
const bool condition = intValue(node, "condition", 0) != 0;
|
||||
if ((condition && !first) || (!condition && !second))
|
||||
error_ = "branch requires both selected inputs: " + id;
|
||||
else node.cache = condition ? *first : *second;
|
||||
} else if (node.operation == "subgraph") {
|
||||
if (!first || !node.subgraph) error_ = "subgraph requires input and graph: " + id;
|
||||
else {
|
||||
node.subgraph->setMaxNodeOutputPoints(maxNodeOutputPoints_);
|
||||
node.subgraph->setComputePolicy(computePolicy_);
|
||||
node.subgraph->setComputeMinimumPoints(computeMinimumPoints_);
|
||||
if (!node.subgraph->setNodePoints(node.subgraphInput, const_cast<PointSet*>(first)))
|
||||
error_ = "subgraph input failed at " + id + ": " + node.subgraph->getError();
|
||||
else {
|
||||
std::unique_ptr<PointSet> result(node.subgraph->execute(node.subgraphOutput));
|
||||
if (!result)
|
||||
error_ = "subgraph failed at " + id + ": " + node.subgraph->getError();
|
||||
else
|
||||
node.cache = *result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error_.empty() && maxNodeOutputPoints_ > 0 &&
|
||||
node.cache.getCount() > maxNodeOutputPoints_) {
|
||||
node.cache.clear();
|
||||
error_ = node.operation + " exceeds node point budget at node: " + id;
|
||||
}
|
||||
if (!error_.empty()) return nullptr;
|
||||
node.cacheValid = true;
|
||||
states[id] = 2;
|
||||
const float elapsed = float(nowNanoseconds() - started) * 0.000001f;
|
||||
node.cacheMetric = makeMetric(id, node.cache, elapsed, false, nodeBackend);
|
||||
metrics_.push_back(node.cacheMetric);
|
||||
return &node.cache;
|
||||
}
|
||||
|
||||
const PointSet* PointGraph::evaluateTransformSegment(const std::string& id,
|
||||
std::unordered_map<std::string, int>& states) {
|
||||
if (!executionPlan_) return nullptr;
|
||||
const auto segmentEntry = executionPlan_->segmentByOutput.find(id);
|
||||
if (segmentEntry == executionPlan_->segmentByOutput.end()) return nullptr;
|
||||
const ExecutionSegment& planned = executionPlan_->segments[segmentEntry->second];
|
||||
if (!planned.gpuTransformChain || planned.nodes.size() < 2) return nullptr;
|
||||
const std::vector<std::string>& chain = planned.nodes;
|
||||
if (std::any_of(chain.begin(), chain.end(),
|
||||
[&](const std::string& nodeId) { return nodes_.at(nodeId).cacheValid; }))
|
||||
return nullptr;
|
||||
const std::string& cursor = nodes_.at(chain.front()).inputs[0];
|
||||
|
||||
const PointSet* input = evaluate(cursor, states);
|
||||
if (!input) return nullptr;
|
||||
const bool gpuEligible =
|
||||
!input->empty() &&
|
||||
(computePolicy_ == "gpu" || (computePolicy_ == "auto" && input->getCount() >= computeMinimumPoints_));
|
||||
if (!gpuEligible) return nullptr;
|
||||
if (executionNodeBudget_ > 0 && evaluatedNodes_ + int(chain.size()) > executionNodeBudget_) {
|
||||
error_ = "execution node budget exceeded at node: " + id;
|
||||
lastCancelled_ = true;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<PointCompute::Transform> transforms;
|
||||
transforms.reserve(chain.size());
|
||||
for (const std::string& nodeId : chain) {
|
||||
const Node& transformNode = nodes_.at(nodeId);
|
||||
transforms.push_back({floatValue(transformNode, "x", 0.f), floatValue(transformNode, "y", 0.f),
|
||||
floatValue(transformNode, "z", 0.f), floatValue(transformNode, "yaw", 0.f),
|
||||
floatValue(transformNode, "scaleX", 1.f), floatValue(transformNode, "scaleY", 1.f),
|
||||
floatValue(transformNode, "scaleZ", 1.f)});
|
||||
}
|
||||
|
||||
PointSet output;
|
||||
const uint64_t started = nowNanoseconds();
|
||||
if (!pointCompute_->transformChain(*input, output, transforms)) {
|
||||
computeFallbackReason_ = pointCompute_->getError();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
evaluatedNodes_ += int(chain.size());
|
||||
for (const std::string& nodeId : chain) {
|
||||
Node& transformNode = nodes_.at(nodeId);
|
||||
transformNode.deferredTransformValid = true;
|
||||
states[nodeId] = 2;
|
||||
}
|
||||
Node& finalNode = nodes_.at(id);
|
||||
finalNode.cache = std::move(output);
|
||||
finalNode.cacheValid = true;
|
||||
finalNode.deferredTransformValid = false;
|
||||
#ifdef EVENGINE_WEBGPU
|
||||
const std::string backend = "webgpu";
|
||||
#else
|
||||
const std::string backend = "vulkan";
|
||||
#endif
|
||||
const float elapsed = float(nowNanoseconds() - started) * 0.000001f;
|
||||
finalNode.cacheMetric = makeMetric(id, finalNode.cache, elapsed, false, backend);
|
||||
metrics_.push_back(finalNode.cacheMetric);
|
||||
return &finalNode.cache;
|
||||
}
|
||||
|
||||
bool PointGraph::validateNode(const std::string& id, std::unordered_map<std::string, int>& states) {
|
||||
if (states[id] == 2) return true;
|
||||
if (states[id] == 1) {
|
||||
error_ = "cycle at node: " + id;
|
||||
return false;
|
||||
}
|
||||
const auto found = nodes_.find(id);
|
||||
if (found == nodes_.end() || !operationKnown(found->second.operation)) {
|
||||
error_ = "invalid node: " + id;
|
||||
return false;
|
||||
}
|
||||
states[id] = 1;
|
||||
for (const auto& input : found->second.inputs) {
|
||||
if (!input.empty() && !validateNode(input, states)) return false;
|
||||
}
|
||||
const auto& node = found->second;
|
||||
if (node.operation == "input" && !node.hasPoints) error_ = "input node has no points: " + id;
|
||||
else if ((node.operation == "spatial.sample" || node.operation == "spatial.filter" ||
|
||||
node.operation == "spatial.project" || node.operation == "biome.generate") &&
|
||||
!node.spatial)
|
||||
error_ = "node has no spatial data: " + id;
|
||||
else if (node.operation == "biome.generate" && !node.biomeRules)
|
||||
error_ = "node has no biome rules: " + id;
|
||||
else if (node.operation == "grammar.generate" && !node.shapeGrammar)
|
||||
error_ = "node has no shape grammar: " + id;
|
||||
else if (node.operation == "subgraph" && (!node.subgraph || node.inputs[0].empty()))
|
||||
error_ = "subgraph is not configured: " + id;
|
||||
else if (node.operation == "subgraph") {
|
||||
PointGraph nested = *node.subgraph;
|
||||
PointSet externalInput;
|
||||
if (!nested.setNodePoints(node.subgraphInput, &externalInput) || !nested.validate())
|
||||
error_ = "invalid subgraph at " + id + ": " + nested.getError();
|
||||
} else {
|
||||
const auto* spec = operationSpec(node.operation);
|
||||
for (int index = 0; spec && index < spec->inputs; ++index)
|
||||
if (node.inputs[index].empty()) {
|
||||
error_ = node.operation + " requires input " + std::to_string(index) + ": " + id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!error_.empty()) return false;
|
||||
states[id] = 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PointGraph::invalidate() { clearCache(); }
|
||||
|
||||
void PointGraph::invalidateFrom(const std::string& id) {
|
||||
|
||||
Reference in new issue
Block a user