243 lines
10 KiB
C++
243 lines
10 KiB
C++
#include "procgen/Biome.h"
|
|
#include "procgen/PointGraph.h"
|
|
#include "procgen/ShapeGrammar.h"
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
using namespace eve::procgen;
|
|
|
|
namespace {
|
|
|
|
bool pointSetsEqual(const PointSet& first, const PointSet& second) {
|
|
if (first.getCount() != second.getCount()) return false;
|
|
if (first.attributes().columnCount() != second.attributes().columnCount()) return false;
|
|
for (int index = 0; index < first.getCount(); ++index) {
|
|
const ProcgenPoint& a = first.points()[size_t(index)];
|
|
const ProcgenPoint& b = second.points()[size_t(index)];
|
|
if (a.x != b.x || a.y != b.y || a.z != b.z || a.normalX != b.normalX || a.normalY != b.normalY ||
|
|
a.normalZ != b.normalZ || a.yaw != b.yaw || a.scaleX != b.scaleX || a.scaleY != b.scaleY ||
|
|
a.scaleZ != b.scaleZ || a.density != b.density || a.seed != b.seed)
|
|
return false;
|
|
for (size_t column = 0; column < first.attributes().columnCount(); ++column) {
|
|
const std::string name(first.attributes().columnName(column));
|
|
if (first.attributes().typeOf(name) != second.attributes().typeOf(name) ||
|
|
first.getAttributeType(index, name) != second.getAttributeType(index, name))
|
|
return false;
|
|
switch (*first.attributes().typeOf(name)) {
|
|
case ProcgenAttributeType::Float:
|
|
if (first.attributes().getFloat(index, name) != second.attributes().getFloat(index, name))
|
|
return false;
|
|
break;
|
|
case ProcgenAttributeType::Int:
|
|
if (first.attributes().getInt(index, name) != second.attributes().getInt(index, name)) return false;
|
|
break;
|
|
case ProcgenAttributeType::Bool:
|
|
if (first.attributes().getBool(index, name) != second.attributes().getBool(index, name))
|
|
return false;
|
|
break;
|
|
case ProcgenAttributeType::String:
|
|
if (first.getStringAttribute(index, name, {}) != second.getStringAttribute(index, name, {}))
|
|
return false;
|
|
break;
|
|
case ProcgenAttributeType::Vector: {
|
|
const auto firstValue = first.attributes().getVector(index, name);
|
|
const auto secondValue = second.attributes().getVector(index, name);
|
|
if (firstValue.has_value() != secondValue.has_value()) return false;
|
|
if (firstValue && (firstValue->x != secondValue->x || firstValue->y != secondValue->y ||
|
|
firstValue->z != secondValue->z))
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("procgen.pointGraphStress.resumesDeepGraphAcrossBudgetWindows") {
|
|
constexpr int nodeCount = 64;
|
|
PointSet input;
|
|
input.add(0.f, 0.f, 0.f);
|
|
PointGraph graph;
|
|
CHECK(graph.addNode("source", "input"));
|
|
CHECK(graph.setNodePoints("source", &input));
|
|
std::string previous = "source";
|
|
for (int index = 0; index < nodeCount; ++index) {
|
|
const std::string id = "move-" + std::to_string(index);
|
|
CHECK(graph.addNode(id, "transform"));
|
|
CHECK(graph.connect(previous, id));
|
|
CHECK(graph.setNodeFloat(id, "x", 0.25f));
|
|
previous = id;
|
|
}
|
|
CHECK(graph.validate());
|
|
graph.setExecutionNodeBudget(17);
|
|
|
|
std::unique_ptr<PointSet> output;
|
|
int budgetWindows = 0;
|
|
while (!output && budgetWindows < 20) {
|
|
output.reset(graph.execute(previous));
|
|
++budgetWindows;
|
|
if (!output) CHECK(graph.wasCancelled());
|
|
}
|
|
REQUIRE(bool(output));
|
|
CHECK_EQ(output->getX(0), 16.f);
|
|
CHECK_EQ(budgetWindows, 4);
|
|
CHECK(graph.getCacheHitCount() > 0);
|
|
}
|
|
|
|
TEST_CASE("procgen.pointGraphStress.computePolicyPreservesDeterministicCpuPath") {
|
|
PointSet input;
|
|
input.add(2.f, 3.f, 4.f);
|
|
PointGraph graph;
|
|
CHECK(graph.addNode("source", "input"));
|
|
CHECK(graph.addNode("move", "transform"));
|
|
CHECK(graph.connect("source", "move"));
|
|
CHECK(graph.setNodePoints("source", &input));
|
|
CHECK(graph.setNodeFloat("move", "x", 5.f));
|
|
CHECK(!graph.setComputePolicy("invalid"));
|
|
CHECK(graph.setComputePolicy("cpu"));
|
|
graph.setComputeMinimumPoints(0);
|
|
|
|
std::unique_ptr<PointSet> output(graph.execute("move"));
|
|
REQUIRE(bool(output));
|
|
CHECK_EQ(output->getX(0), 7.f);
|
|
CHECK_EQ(graph.getComputePolicy(), "cpu");
|
|
CHECK_EQ(graph.getComputeMinimumPoints(), 0);
|
|
CHECK_EQ(graph.getMetricBackend(graph.getMetricCount() - 1), "cpu");
|
|
CHECK(graph.getComputeFallbackReason().empty());
|
|
CHECK_EQ(graph.getComputeUploadCount(), uint64_t(0));
|
|
CHECK_EQ(graph.getComputeDispatchCount(), uint64_t(0));
|
|
CHECK_EQ(graph.getComputeReadbackCount(), uint64_t(0));
|
|
CHECK_EQ(graph.getComputeBufferReuseCount(), uint64_t(0));
|
|
CHECK_EQ(graph.getComputePeakBufferBytes(), uint64_t(0));
|
|
CHECK_EQ(graph.getLastFusedTransformCount(), 0);
|
|
CHECK(graph.debugReport().find("backend=cpu") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("procgen.pointGraphStress.isBitStableAcrossIndependentBiomeInstances") {
|
|
SpatialData domain = SpatialData::box(-32.f, 0.f, -32.f, 32.f, 0.f, 32.f);
|
|
SpatialData center = SpatialData::sphere(0.f, 0.f, 0.f, 12.f);
|
|
BiomeRules rules;
|
|
CHECK(rules.addLayer("forest", &domain, 1, 0.85f));
|
|
CHECK(rules.addLayer("clearing", ¢er, 5, 0.35f));
|
|
CHECK(rules.addAsset("forest", "oak", 3.f, 0.8f, 1.3f, true));
|
|
CHECK(rules.addAsset("forest", "pine", 1.f, 0.9f, 1.4f, true));
|
|
CHECK(rules.addAsset("clearing", "rock", 1.f, 0.7f, 1.1f, true));
|
|
|
|
PointGraph asset;
|
|
CHECK(asset.addNode("biome", "biome.generate"));
|
|
CHECK(asset.setNodeFloat("biome", "spacing", 1.f));
|
|
CHECK(asset.setNodeFloat("biome", "jitter", 0.75f));
|
|
CHECK(asset.setNodeInt("biome", "seed", 0x5a17));
|
|
std::unique_ptr<PointGraph> first(asset.instantiate());
|
|
std::unique_ptr<PointGraph> second(asset.instantiate());
|
|
REQUIRE(bool(first));
|
|
REQUIRE(bool(second));
|
|
CHECK(first->setNodeSpatial("biome", &domain));
|
|
CHECK(first->setNodeBiomeRules("biome", &rules));
|
|
CHECK(second->setNodeSpatial("biome", &domain));
|
|
CHECK(second->setNodeBiomeRules("biome", &rules));
|
|
|
|
std::unique_ptr<PointSet> firstOutput(first->execute("biome"));
|
|
std::unique_ptr<PointSet> secondOutput(second->execute("biome"));
|
|
REQUIRE(bool(firstOutput));
|
|
REQUIRE(bool(secondOutput));
|
|
CHECK(firstOutput->getCount() > 3000);
|
|
CHECK(firstOutput->getCount() < 4225);
|
|
CHECK(pointSetsEqual(*firstOutput, *secondOutput));
|
|
}
|
|
|
|
TEST_CASE("procgen.pointGraphStress.recoversAfterExternalRuleAndDataErrors") {
|
|
PointSet path;
|
|
path.add(0.f, 0.f, 0.f);
|
|
path.add(10.f, 0.f, 0.f);
|
|
ShapeGrammar grammar;
|
|
CHECK(grammar.addModule("A", "wall", 2.f));
|
|
PointGraph graph;
|
|
CHECK(graph.addNode("path", "input"));
|
|
CHECK(graph.addNode("facade", "grammar.generate"));
|
|
CHECK(graph.connect("path", "facade"));
|
|
CHECK(graph.setNodeString("facade", "grammar", "Missing+"));
|
|
CHECK(!graph.execute("facade"));
|
|
CHECK(graph.getError().find("no points") != std::string::npos);
|
|
|
|
CHECK(graph.setNodePoints("path", &path));
|
|
CHECK(graph.setNodeShapeGrammar("facade", &grammar));
|
|
CHECK(!graph.execute("facade"));
|
|
CHECK(graph.getError().find("unknown module") != std::string::npos);
|
|
CHECK(graph.setNodeString("facade", "grammar", "A+"));
|
|
std::unique_ptr<PointSet> recovered(graph.execute("facade"));
|
|
REQUIRE(bool(recovered));
|
|
CHECK_EQ(recovered->getCount(), 5);
|
|
CHECK_EQ(recovered->getStringAttribute(0, "asset", ""), std::string("wall"));
|
|
}
|
|
|
|
TEST_CASE("procgen.pointGraphStress.rejectsOversizedOutputsBeforeSpatialAllocation") {
|
|
SpatialData domain = SpatialData::box(0.f, 0.f, 0.f, 100.f, 0.f, 100.f);
|
|
PointGraph graph;
|
|
CHECK(graph.addNode("sample", "spatial.sample"));
|
|
CHECK(graph.setNodeSpatial("sample", &domain));
|
|
CHECK(graph.setNodeFloat("sample", "spacing", 1.f));
|
|
graph.setMaxNodeOutputPoints(1000);
|
|
CHECK_EQ(graph.getMaxNodeOutputPoints(), 1000);
|
|
CHECK(!graph.execute("sample"));
|
|
CHECK(graph.getError().find("point budget") != std::string::npos);
|
|
CHECK(!graph.getNodeOutput("sample"));
|
|
|
|
graph.setMaxNodeOutputPoints(11000);
|
|
std::unique_ptr<PointSet> output(graph.execute("sample"));
|
|
REQUIRE(bool(output));
|
|
CHECK_EQ(output->getCount(), 10201);
|
|
|
|
graph.setMaxNodeOutputPoints(1000);
|
|
CHECK(!graph.execute("sample"));
|
|
CHECK(graph.getError().find("point budget") != std::string::npos);
|
|
|
|
Heightmap heightmap(11, 11);
|
|
heightmap.setHeight(10, 10, 100.f);
|
|
SpatialData terrain = SpatialData::heightfield(heightmap, 0.f, 0.f, 1.f, 1.f);
|
|
PointGraph terrainGraph;
|
|
CHECK(terrainGraph.addNode("sample", "spatial.sample"));
|
|
CHECK(terrainGraph.setNodeSpatial("sample", &terrain));
|
|
terrainGraph.setMaxNodeOutputPoints(121);
|
|
output.reset(terrainGraph.execute("sample"));
|
|
REQUIRE(bool(output));
|
|
CHECK_EQ(output->getCount(), 121);
|
|
}
|
|
|
|
TEST_CASE("procgen.pointGraphStress.preflightsInputMergeAndCopyAllocations") {
|
|
PointSet first;
|
|
PointSet second;
|
|
for (int index = 0; index < 60; ++index) {
|
|
first.add(float(index), 0.f, 0.f);
|
|
second.add(0.f, 0.f, float(index));
|
|
}
|
|
PointGraph graph;
|
|
CHECK(graph.addNode("first", "input"));
|
|
CHECK(graph.addNode("second", "input"));
|
|
CHECK(graph.addNode("merge", "merge"));
|
|
CHECK(graph.addNode("copy", "copy.points"));
|
|
graph.setMaxNodeOutputPoints(100);
|
|
CHECK(graph.setNodePoints("first", &first));
|
|
CHECK(graph.setNodePoints("second", &second));
|
|
CHECK(graph.connect("first", "merge", 0));
|
|
CHECK(graph.connect("second", "merge", 1));
|
|
CHECK(graph.connect("first", "copy", 0));
|
|
CHECK(graph.connect("second", "copy", 1));
|
|
CHECK(graph.setNodeInt("copy", "maxPoints", 10000));
|
|
CHECK(!graph.execute("merge"));
|
|
CHECK(graph.getError().find("point budget") != std::string::npos);
|
|
CHECK(!graph.execute("copy"));
|
|
CHECK(graph.getError().find("point budget") != std::string::npos);
|
|
|
|
PointSet oversized;
|
|
for (int index = 0; index < 101; ++index) oversized.add(float(index), 0.f, 0.f);
|
|
CHECK(!graph.setNodePoints("first", &oversized));
|
|
CHECK(graph.getError().find("input exceeds") != std::string::npos);
|
|
}
|