Files
EVEngine/test/procgen_runtime_generation.cpp
T

837 lines
34 KiB
C++

#include "procgen/Procgen.h"
#include "procgen/RuntimeGeneration.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <simplesquirrel/simplesquirrel.hpp>
#include <memory>
#include <utility>
using namespace eve::procgen;
namespace {
/** @brief Owns a module-backed runtime-generation handle for one test. */
class RuntimeLease {
public:
RuntimeLease() = default;
RuntimeLease(Procgen& owner, ProcgenRuntimeGenerationHandleRef handle) : owner_(&owner), handle_(handle) {}
RuntimeLease(const RuntimeLease&) = delete;
RuntimeLease& operator=(const RuntimeLease&) = delete;
RuntimeLease(RuntimeLease&& other) noexcept : owner_(other.owner_), handle_(other.handle_) {
other.owner_ = nullptr;
other.handle_ = {};
}
RuntimeLease& operator=(RuntimeLease&& other) noexcept {
if (this == &other) return *this;
reset();
owner_ = other.owner_;
handle_ = other.handle_;
other.owner_ = nullptr;
other.handle_ = {};
return *this;
}
~RuntimeLease() { reset(); }
/** @brief Releases the scheduler handle and explicitly observes the Result. */
void reset() noexcept {
if (!owner_ || !handle_.isValid()) return;
auto result = owner_->release(handle_);
result.ignore("test runtime-generation cleanup");
owner_ = nullptr;
handle_ = {};
}
/** @brief Provides a short-lived borrowed view of the scheduler. */
[[nodiscard]] eve::script::Borrowed<RuntimeGeneration> view() const noexcept {
return owner_ ? owner_->resolveRuntimeGeneration(handle_) : eve::script::Borrowed<RuntimeGeneration>();
}
/** @brief Preserves the concise `runtime->operation()` test syntax. */
[[nodiscard]] eve::script::Borrowed<RuntimeGeneration> operator->() const noexcept { return view(); }
private:
Procgen* owner_ = nullptr;
ProcgenRuntimeGenerationHandleRef handle_{};
};
/** @brief Converts a checked scheduler allocation Result into a test lease. */
RuntimeLease requireRuntime(Procgen& proc, uint32_t worldSeed) {
auto result = proc.newRuntimeGenerationHandle(worldSeed);
const bool ok = result.ok();
if (!ok) {
const eve::Diagnostic* diagnostic = result.error();
REQUIRE(diagnostic != nullptr);
}
REQUIRE(ok);
return RuntimeLease(proc, std::move(result).takeValue());
}
/** @brief Gives an owning lifetime to a request allocated by the scheduler API. */
using RequestLease = std::unique_ptr<ProcgenCellRequest>;
[[nodiscard]] RequestLease ownRequest(ProcgenCellRequest* request) noexcept { return RequestLease(request); }
} // namespace
TEST_CASE("procgen.runtimeGeneration.partitionsAndPublishesCells") {
Procgen proc;
auto runtime = requireRuntime(proc, 42);
const int nearLevel = runtime->addLevel(10.f, 8.f, 1.5f);
const int farLevel = runtime->addLevel(40.f, 30.f, 2.f);
CHECK_EQ(nearLevel, 0);
CHECK_EQ(farLevel, 1);
CHECK_EQ(runtime->getLevelCount(), 2);
CHECK_EQ(runtime->getLevelCleanupRadius(0), 12.f);
runtime->setMaxGenerating(1);
runtime->setFrameTimeBudget(2.f);
CHECK_EQ(runtime->getFrameTimeBudget(), 2.f);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
CHECK(runtime->getPendingGenerateCount() > 0);
runtime->beginFrame();
auto request = ownRequest(runtime->nextGenerate());
REQUIRE(bool(request));
CHECK_EQ(runtime->getGeneratingCount(), 1);
CHECK(!runtime->nextGenerate());
CHECK_NE(request->getSeed(), uint32_t(0));
CHECK(request->getMaxX() > request->getMinX());
const int level = request->getLevel();
const int x = request->getX();
const int z = request->getZ();
PointSet output;
output.add(request->getMinX(), 0.f, request->getMinZ());
CHECK(runtime->completeGeneration(request.get(), &output));
CHECK(runtime->hasCell(level, x, z));
CHECK_EQ(runtime->getCellRevision(level, x, z), uint64_t(1));
auto stored = std::unique_ptr<PointSet>(runtime->getCellOutput(level, x, z));
REQUIRE(bool(stored));
CHECK_EQ(stored->getCount(), 1);
}
TEST_CASE("procgen.runtimeGeneration.cleansBeyondHysteresisRadius") {
Procgen proc;
auto runtime = requireRuntime(proc, 7);
runtime->addLevel(10.f, 6.f, 2.f);
runtime->updateSource(5.f, 5.f, 0.f, 1.f);
auto generated = ownRequest(runtime->nextGenerate());
REQUIRE(bool(generated));
const int cellX = generated->getX();
const int cellZ = generated->getZ();
PointSet points;
points.add(5.f, 0.f, 5.f);
CHECK(runtime->completeGeneration(generated.get(), &points));
runtime->updateSource(100.f, 100.f, 0.f, 1.f);
CHECK(runtime->getPendingCleanupCount() > 0);
bool cleanedTarget = false;
while (auto cleanup = ownRequest(runtime->nextCleanup())) {
if (cleanup->getX() == cellX && cleanup->getZ() == cellZ) cleanedTarget = true;
CHECK(runtime->completeCleanup(cleanup.get()));
}
CHECK(cleanedTarget);
CHECK(!runtime->hasCell(0, cellX, cellZ));
}
TEST_CASE("procgen.runtimeGeneration.failedWorkCanRetryWithStableSeed") {
Procgen proc;
auto runtime = requireRuntime(proc, 99);
runtime->addLevel(8.f, 5.f, 1.25f);
runtime->updateSource(4.f, 4.f, 1.f, 0.f);
auto first = ownRequest(runtime->nextGenerate());
REQUIRE(bool(first));
const uint32_t seed = first->getSeed();
CHECK(runtime->failGeneration(first.get()));
auto retry = ownRequest(runtime->nextGenerate());
REQUIRE(bool(retry));
CHECK_EQ(retry->getSeed(), seed);
CHECK_EQ(retry->getLevel(), 0);
}
TEST_CASE("procgen.runtimeGeneration.unionsNamedSourcesAndCleansAfterLastSource") {
Procgen proc;
auto runtime = requireRuntime(proc, 123);
runtime->addLevel(10.f, 16.f, 1.5f);
runtime->setMaxGenerating(100);
CHECK(runtime->setGenerationSource("player", 5.f, 5.f, 1.f, 0.f, 1.f));
CHECK(runtime->setGenerationSource("quest", 105.f, 5.f, 0.f, 0.f, 1.f));
CHECK_EQ(runtime->getGenerationSourceCount(), 2);
CHECK_EQ(runtime->getGenerationSourceId(0), std::string("player"));
CHECK_EQ(runtime->getGenerationSourceId(1), std::string("quest"));
bool generatedNearPlayer = false;
bool generatedNearQuest = false;
PointSet empty;
while (auto request = ownRequest(runtime->nextGenerate())) {
if (request->getX() >= -1 && request->getX() <= 1) generatedNearPlayer = true;
if (request->getX() >= 9 && request->getX() <= 11) generatedNearQuest = true;
CHECK(runtime->completeGeneration(request.get(), &empty));
}
CHECK(generatedNearPlayer);
CHECK(generatedNearQuest);
CHECK(runtime->removeGenerationSource("player"));
bool cleanupNearPlayer = false;
bool cleanupNearQuest = false;
while (auto request = ownRequest(runtime->nextCleanup())) {
if (request->getX() >= -1 && request->getX() <= 1) cleanupNearPlayer = true;
if (request->getX() >= 9 && request->getX() <= 11) cleanupNearQuest = true;
CHECK(runtime->completeCleanup(request.get()));
}
CHECK(cleanupNearPlayer);
CHECK(!cleanupNearQuest);
}
TEST_CASE("procgen.runtimeGeneration.frustumCullsFarBehindButKeepsNearCells") {
Procgen proc;
auto runtime = requireRuntime(proc, 9);
runtime->addLevel(10.f, 35.f, 1.25f);
runtime->setMaxGenerating(100);
runtime->setFrustumCulling(true, 45.f, 11.f);
CHECK(runtime->isFrustumCullingEnabled());
CHECK_EQ(runtime->getFrustumHalfAngle(), 45.f);
CHECK_EQ(runtime->getFrustumBehindRadius(), 11.f);
CHECK(runtime->setGenerationSource("camera", 5.f, 5.f, 1.f, 0.f, 1.f));
bool foundFarBehind = false;
bool foundNearBehind = false;
bool foundForward = false;
PointSet empty;
while (auto request = ownRequest(runtime->nextGenerate())) {
const float centerX = (request->getMinX() + request->getMaxX()) * 0.5f;
if (centerX < -6.f) foundFarBehind = true;
if (centerX == -5.f) foundNearBehind = true;
if (centerX > 5.f) foundForward = true;
CHECK(runtime->completeGeneration(request.get(), &empty));
}
CHECK(!foundFarBehind);
CHECK(foundNearBehind);
CHECK(foundForward);
}
TEST_CASE("procgen.runtimeGeneration.rejectsStaleAsyncGenerationTickets") {
Procgen proc;
auto runtime = requireRuntime(proc, 77);
runtime->addLevel(10.f, 6.f, 1.5f);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto stale = ownRequest(runtime->nextGenerate());
REQUIRE(bool(stale));
const uint64_t staleTicket = stale->getTicket();
CHECK(runtime->isRequestCurrent(stale.get()));
runtime->updateSource(100.f, 100.f, 1.f, 0.f);
CHECK(!runtime->isRequestCurrent(stale.get()));
CHECK_EQ(runtime->getCancelledGenerationCount(), 1);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto current = ownRequest(runtime->nextGenerate());
REQUIRE(bool(current));
CHECK(runtime->isRequestCurrent(current.get()));
CHECK_NE(current->getTicket(), staleTicket);
PointSet output;
CHECK(!runtime->completeGeneration(stale.get(), &output));
CHECK(runtime->completeGeneration(current.get(), &output));
CHECK(!runtime->isRequestCurrent(current.get()));
CHECK(runtime->debugReport().find("cancelledGeneration=1") != std::string::npos);
}
TEST_CASE("procgen.runtimeGeneration.rejectsStaleAsyncCleanupTickets") {
Procgen proc;
auto runtime = requireRuntime(proc, 78);
runtime->addLevel(10.f, 6.f, 1.5f);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto generated = ownRequest(runtime->nextGenerate());
REQUIRE(bool(generated));
PointSet output;
CHECK(runtime->completeGeneration(generated.get(), &output));
runtime->updateSource(100.f, 100.f, 1.f, 0.f);
auto stale = ownRequest(runtime->nextCleanup());
REQUIRE(bool(stale));
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
runtime->updateSource(100.f, 100.f, 1.f, 0.f);
std::vector<const ProcgenCellRequest*> staleRequests{stale.get()};
auto staleResult = runtime->completeCleanupsAtomic(staleRequests);
CHECK(!staleResult.ok());
CHECK(!runtime->completeCleanup(stale.get()));
auto current = ownRequest(runtime->nextCleanup());
REQUIRE(bool(current));
CHECK_NE(current->getTicket(), stale->getTicket());
CHECK(runtime->completeCleanup(current.get()));
}
TEST_CASE("procgen.runtimeGeneration.completesCleanupTicketsAtomically") {
Procgen proc;
auto runtime = requireRuntime(proc, 79);
runtime->addLevel(10.f, 16.f, 1.5f);
runtime->setMaxGenerating(8);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto firstGenerated = ownRequest(runtime->nextGenerate());
auto secondGenerated = ownRequest(runtime->nextGenerate());
REQUIRE(bool(firstGenerated));
REQUIRE(bool(secondGenerated));
PointSet output;
REQUIRE(runtime->completeGeneration(firstGenerated.get(), &output));
REQUIRE(runtime->completeGeneration(secondGenerated.get(), &output));
runtime->updateSource(1000.f, 1000.f, 1.f, 0.f);
auto firstCleanup = ownRequest(runtime->nextCleanup());
auto secondCleanup = ownRequest(runtime->nextCleanup());
REQUIRE(bool(firstCleanup));
REQUIRE(bool(secondCleanup));
std::vector<const ProcgenCellRequest*> empty;
auto emptyResult = runtime->completeCleanupsAtomic(empty);
REQUIRE(!emptyResult.ok());
CHECK(runtime->isRequestCurrent(firstCleanup.get()));
CHECK(runtime->isRequestCurrent(secondCleanup.get()));
std::vector<const ProcgenCellRequest*> duplicate{firstCleanup.get(), firstCleanup.get()};
auto duplicateResult = runtime->completeCleanupsAtomic(duplicate);
REQUIRE(!duplicateResult.ok());
CHECK(runtime->isRequestCurrent(firstCleanup.get()));
CHECK(runtime->isRequestCurrent(secondCleanup.get()));
auto otherRuntime = requireRuntime(proc, 80);
otherRuntime->addLevel(10.f, 16.f, 1.5f);
std::vector<const ProcgenCellRequest*> wrongOwner{firstCleanup.get()};
auto wrongOwnerResult = otherRuntime->completeCleanupsAtomic(wrongOwner);
REQUIRE(!wrongOwnerResult.ok());
CHECK(runtime->isRequestCurrent(firstCleanup.get()));
std::vector<const ProcgenCellRequest*> requests{firstCleanup.get(), secondCleanup.get()};
auto completed = runtime->completeCleanupsAtomic(requests);
REQUIRE(completed.ok());
CHECK_EQ(completed.value(), uint64_t(2));
CHECK(!runtime->isRequestCurrent(firstCleanup.get()));
CHECK(!runtime->isRequestCurrent(secondCleanup.get()));
}
TEST_CASE("procgen.runtimeGeneration.squirrelCompletesCleanupTicketsAtomically") {
ssq::VM vm(1024, ssq::Libs::ALL);
eve::ModuleManager::expose(vm);
vm.run(vm.compileSource(R"(
result <- "fail";
local procgen = eve.Procgen();
local runtimeResult = procgen.newRuntimeGeneration(81);
local pointsResult = procgen.sampleGrid(1, 1, 1.0, 18, 0.0);
if (runtimeResult.ok && pointsResult.ok) {
local runtime = runtimeResult.value;
local points = pointsResult.value;
runtime.addLevel(10.0, 16.0, 1.5);
runtime.setMaxGenerating(8);
runtime.updateSource(5.0, 5.0, 1.0, 0.0);
local first = runtime.nextGenerate();
local second = runtime.nextGenerate();
if (first != null && second != null && runtime.completeGeneration(first, points) &&
runtime.completeGeneration(second, points)) {
runtime.updateSource(1000.0, 1000.0, 1.0, 0.0);
local firstCleanup = runtime.nextCleanup();
local secondCleanup = runtime.nextCleanup();
local completed = runtime.completeCleanupsAtomic([firstCleanup, secondCleanup]);
if (completed.ok && completed.value == "2" &&
!runtime.isRequestCurrent(firstCleanup) && !runtime.isRequestCurrent(secondCleanup))
result = "ok";
}
}
)"));
CHECK_EQ(vm.find("result").toString(), std::string("ok"));
}
TEST_CASE("procgen.runtimeGeneration.enforcesResidentCellReservations") {
Procgen proc;
auto runtime = requireRuntime(proc, 91);
runtime->addLevel(10.f, 30.f, 1.5f);
runtime->setMaxGenerating(100);
runtime->setMaxActiveCells(2);
CHECK_EQ(runtime->getMaxActiveCells(), 2);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto first = ownRequest(runtime->nextGenerate());
auto second = ownRequest(runtime->nextGenerate());
REQUIRE(bool(first));
REQUIRE(bool(second));
CHECK(!runtime->nextGenerate());
CHECK_EQ(runtime->getGeneratingCount(), 2);
PointSet empty;
CHECK(runtime->completeGeneration(first.get(), &empty));
CHECK(runtime->completeGeneration(second.get(), &empty));
CHECK_EQ(runtime->getActiveCellCount(), 2);
CHECK(!runtime->nextGenerate());
CHECK(runtime->debugReport().find("maxActive=2") != std::string::npos);
runtime->setMaxActiveCells(0);
CHECK_EQ(runtime->getMaxActiveCells(), 0);
auto unlimited = ownRequest(runtime->nextGenerate());
REQUIRE(bool(unlimited));
}
TEST_CASE("procgen.runtimeGeneration.enforcesPointMemoryBudgets") {
Procgen proc;
auto runtime = requireRuntime(proc, 92);
runtime->addLevel(10.f, 25.f, 1.5f);
runtime->setMaxGenerating(100);
runtime->setMaxPointsPerCell(2);
runtime->setMaxResidentPoints(2);
CHECK_EQ(runtime->getMaxPointsPerCell(), 2);
CHECK_EQ(runtime->getMaxResidentPoints(), 2);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto first = ownRequest(runtime->nextGenerate());
REQUIRE(bool(first));
PointSet oversized;
oversized.add(0.f, 0.f, 0.f);
oversized.add(1.f, 0.f, 0.f);
oversized.add(2.f, 0.f, 0.f);
CHECK(!runtime->completeGeneration(first.get(), &oversized));
CHECK_EQ(runtime->getRejectedOutputCount(), 1);
CHECK(runtime->failGeneration(first.get()));
auto retry = ownRequest(runtime->nextGenerate());
REQUIRE(bool(retry));
PointSet accepted;
accepted.add(0.f, 0.f, 0.f);
accepted.add(1.f, 0.f, 0.f);
CHECK(runtime->completeGeneration(retry.get(), &accepted));
CHECK_EQ(runtime->getResidentPointCount(), 2);
auto second = ownRequest(runtime->nextGenerate());
REQUIRE(bool(second));
PointSet one;
one.add(0.f, 0.f, 0.f);
CHECK(!runtime->completeGeneration(second.get(), &one));
CHECK_EQ(runtime->getRejectedOutputCount(), 2);
CHECK_EQ(runtime->getPendingCleanupCount(), 1);
runtime->refreshGenerationSources();
CHECK(!runtime->completeGeneration(second.get(), &one));
CHECK_EQ(runtime->getRejectedOutputCount(), 3);
CHECK_EQ(runtime->getPendingCleanupCount(), 1);
auto cleanup = ownRequest(runtime->nextCleanup());
REQUIRE(bool(cleanup));
CHECK(runtime->completeCleanup(cleanup.get()));
CHECK_EQ(runtime->getResidentPointCount(), 0);
CHECK(runtime->completeGeneration(second.get(), &one));
CHECK_EQ(runtime->getResidentPointCount(), 1);
CHECK(runtime->debugReport().find("residentPoints=1") != std::string::npos);
}
TEST_CASE("procgen.runtimeGeneration.appliesIdentityDeltaAtomically") {
Procgen proc;
auto runtime = requireRuntime(proc, 930);
runtime->addLevel(10.f, 6.f, 1.5f);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto generated = ownRequest(runtime->nextGenerate());
REQUIRE(bool(generated));
const int level = generated->getLevel();
const int x = generated->getX();
const int z = generated->getZ();
PointSet initial = sampleGridPoints(2, 2, 1.f, 11, 0.f);
CHECK(runtime->completeGeneration(generated.get(), &initial));
PointSet updated;
updated.appendPointFrom(initial, 2).expect("runtime delta reordered point");
updated.appendPointFrom(initial, 0).expect("runtime delta updated point");
updated.setPosition(1, 7.f, 2.f, -3.f);
ProcgenPoint added;
added.id = derivePointId(991, 1);
added.seed = 88;
const int addedIndex = updated.appendPoint(added);
(void)addedIndex;
auto committed = runtime->applyCellUpdate(level, x, z, 1, updated);
REQUIRE(committed.ok());
CHECK_EQ(committed.value(), std::uint64_t(2));
CHECK_EQ(runtime->getCellRevision(level, x, z), std::uint64_t(2));
auto delta = std::unique_ptr<PointDelta>(runtime->getCellDelta(level, x, z));
REQUIRE(bool(delta));
CHECK_EQ(delta->added.getCount(), 1);
CHECK_EQ(delta->updated.getCount(), 1);
CHECK_EQ(delta->removed.size(), std::size_t(2));
auto stored = std::unique_ptr<PointSet>(runtime->getCellOutput(level, x, z));
REQUIRE(bool(stored));
auto expectedFingerprint = fingerprintPointSet(updated);
auto actualFingerprint = fingerprintPointSet(*stored);
REQUIRE(expectedFingerprint.ok());
REQUIRE(actualFingerprint.ok());
CHECK_EQ(actualFingerprint.value(), expectedFingerprint.value());
}
TEST_CASE("procgen.runtimeGeneration.rejectsStaleAndOverBudgetDeltaWithoutMutation") {
Procgen proc;
auto runtime = requireRuntime(proc, 931);
runtime->addLevel(10.f, 6.f, 1.5f);
runtime->setMaxPointsPerCell(2);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto generated = ownRequest(runtime->nextGenerate());
REQUIRE(bool(generated));
const int level = generated->getLevel();
const int x = generated->getX();
const int z = generated->getZ();
PointSet initial = sampleGridPoints(2, 1, 1.f, 12, 0.f);
CHECK(runtime->completeGeneration(generated.get(), &initial));
PointSet changed = initial;
changed.setPosition(0, 4.f, 0.f, 0.f);
auto stale = runtime->applyCellUpdate(level, x, z, 9, changed);
CHECK(!stale.ok());
CHECK_EQ(runtime->getCellRevision(level, x, z), std::uint64_t(1));
ProcgenPoint extra;
extra.id = derivePointId(932, 1);
const int extraIndex = changed.appendPoint(extra);
(void)extraIndex;
auto overBudget = runtime->applyCellUpdate(level, x, z, 1, changed);
CHECK(!overBudget.ok());
CHECK_EQ(runtime->getCellRevision(level, x, z), std::uint64_t(1));
CHECK(!runtime->getCellDelta(level, x, z));
auto stored = std::unique_ptr<PointSet>(runtime->getCellOutput(level, x, z));
REQUIRE(bool(stored));
CHECK_EQ(stored->getX(0), initial.getX(0));
}
TEST_CASE("procgen.runtimeGeneration.migratesLegacyCellIdsBeforeIncrementalUpdates") {
Procgen proc;
auto runtime = requireRuntime(proc, 933);
runtime->addLevel(10.f, 6.f, 1.5f);
runtime->updateSource(5.f, 5.f, 1.f, 0.f);
auto generated = ownRequest(runtime->nextGenerate());
REQUIRE(bool(generated));
const int level = generated->getLevel();
const int x = generated->getX();
const int z = generated->getZ();
PointSet legacy;
legacy.add(1.f, 0.f, 2.f);
legacy.add(3.f, 0.f, 4.f);
CHECK(runtime->completeGeneration(generated.get(), &legacy));
auto rejected = runtime->applyCellUpdate(level, x, z, 1, legacy);
CHECK(!rejected.ok());
auto migrated = runtime->migrateCellPointIds(level, x, z, 1);
REQUIRE(migrated.ok());
CHECK_EQ(migrated.value(), std::uint64_t(2));
auto stored = std::unique_ptr<PointSet>(runtime->getCellOutput(level, x, z));
REQUIRE(bool(stored));
CHECK_NE(stored->getPointId(0), std::uint64_t(0));
CHECK_NE(stored->getPointId(1), std::uint64_t(0));
CHECK_NE(stored->getPointId(0), stored->getPointId(1));
}
TEST_CASE("procgen.runtimeGeneration.squirrelAppliesAndInspectsCellDelta") {
ssq::VM vm(1024, ssq::Libs::ALL);
eve::ModuleManager::expose(vm);
vm.run(vm.compileSource(R"(
result <- "fail";
local procgen = eve.Procgen();
local runtimeResult = procgen.newRuntimeGeneration(934);
local pointsResult = procgen.sampleGrid(2, 1, 1.0, 17, 0.0);
if (runtimeResult.ok && pointsResult.ok) {
local runtime = runtimeResult.value;
local points = pointsResult.value;
runtime.addLevel(10.0, 6.0, 1.5);
runtime.updateSource(5.0, 5.0, 1.0, 0.0);
local request = runtime.nextGenerate();
if (request != null && runtime.completeGeneration(request, points)) {
points.setPosition(0, 9.0, 2.0, -3.0);
local update = runtime.applyCellUpdate(
request.getLevel(), request.getX(), request.getZ(), "1", points);
local delta = runtime.getCellDelta(request.getLevel(), request.getX(), request.getZ());
if (update.ok && update.value == "2" && delta != null &&
delta.getAddedCount() == 0 && delta.getUpdatedCount() == 1 &&
delta.getRemovedCount() == 0 && delta.getTargetCount() == 2 &&
delta.getBaseFingerprint() != delta.getTargetFingerprint() &&
delta.getUpdated().getPointId(0) == points.getPointId(0))
result = "ok";
}
}
)"));
CHECK_EQ(vm.find("result").toString(), std::string("ok"));
}
TEST_CASE("procgen.runtimeGeneration.breaksEqualPriorityTiesDeterministically") {
Procgen proc;
auto first = requireRuntime(proc, 17);
auto second = requireRuntime(proc, 17);
for (RuntimeLease* runtime : {&first, &second}) {
(*runtime)->addLevel(10.f, 25.f, 1.5f);
(*runtime)->setMaxGenerating(100);
(*runtime)->updateSource(5.f, 5.f, 0.f, 0.f);
}
for (int index = 0; index < 12; ++index) {
auto a = ownRequest(first->nextGenerate());
auto b = ownRequest(second->nextGenerate());
REQUIRE(bool(a));
REQUIRE(bool(b));
CHECK_EQ(a->getLevel(), b->getLevel());
CHECK_EQ(a->getX(), b->getX());
CHECK_EQ(a->getZ(), b->getZ());
}
}
TEST_CASE("procgen.runtimeGeneration.issuesLargeQueuesWithConstantTimeCounters") {
RuntimeGeneration runtime(1701);
runtime.addLevel(1.f, 64.f, 1.25f);
runtime.setMaxGenerating(20000);
runtime.updateSource(0.5f, 0.5f, 0.f, 0.f);
const int pending = runtime.getPendingGenerateCount();
REQUIRE(pending > 12000);
std::vector<std::unique_ptr<ProcgenCellRequest>> requests;
requests.reserve(size_t(pending));
while (auto request = ownRequest(runtime.nextGenerate())) requests.push_back(std::move(request));
CHECK_EQ(int(requests.size()), pending);
CHECK_EQ(runtime.getPendingGenerateCount(), 0);
CHECK_EQ(runtime.getGeneratingCount(), pending);
PointSet empty;
for (auto& request : requests) CHECK(runtime.completeGeneration(request.get(), &empty));
CHECK_EQ(runtime.getGeneratingCount(), 0);
CHECK_EQ(runtime.getActiveCellCount(), pending);
runtime.updateSource(10000.f, 10000.f, 0.f, 0.f);
CHECK_EQ(runtime.getActiveCellCount(), 0);
CHECK_EQ(runtime.getPendingCleanupCount(), pending);
}
TEST_CASE("procgen.runtimeGeneration.stagesBudgetedRefreshUntilAtomicCommit") {
RuntimeGeneration runtime(1702);
runtime.addLevel(10.f, 25.f, 1.5f);
runtime.setRefreshWorkBudget(3);
CHECK_EQ(runtime.getRefreshWorkBudget(), 3);
runtime.updateSource(5.f, 5.f, 1.f, 0.f);
CHECK(runtime.isRefreshPending());
CHECK_EQ(runtime.getCommittedRefreshRevision(), uint64_t(0));
CHECK_EQ(runtime.getPendingGenerateCount(), 0);
uint64_t processed = 0;
while (runtime.isRefreshPending()) {
auto advanced = runtime.continueGenerationRefresh();
REQUIRE(advanced.ok());
CHECK(advanced.value() <= uint64_t(3));
processed += advanced.value();
}
CHECK(processed > 3);
CHECK_EQ(runtime.getCommittedRefreshRevision(), uint64_t(1));
CHECK(runtime.getPendingGenerateCount() > 0);
}
TEST_CASE("procgen.runtimeGeneration.coalescesMovingSourcesWithoutRefreshStarvation") {
RuntimeGeneration runtime(1703);
runtime.addLevel(10.f, 6.f, 1.5f);
runtime.setRefreshWorkBudget(1);
runtime.updateSource(5.f, 5.f, 1.f, 0.f);
for (int index = 1; index <= 12; ++index) runtime.updateSource(float(index * 100 + 5), 5.f, 1.f, 0.f);
CHECK(runtime.getCommittedRefreshRevision() > uint64_t(0));
CHECK(runtime.isRefreshPending());
runtime.updateSource(1005.f, 5.f, 1.f, 0.f);
int steps = 0;
while (runtime.isRefreshPending() && steps++ < 64) REQUIRE(runtime.continueGenerationRefresh().ok());
REQUIRE(!runtime.isRefreshPending());
auto latest = ownRequest(runtime.nextGenerate());
REQUIRE(bool(latest));
CHECK(latest->getX() >= 99);
}
TEST_CASE("procgen.runtimeGeneration.squirrelAdvancesBudgetedRefresh") {
ssq::VM vm(1024, ssq::Libs::ALL);
eve::ModuleManager::expose(vm);
vm.run(vm.compileSource(R"(
result <- "fail";
local procgen = eve.Procgen();
local created = procgen.newRuntimeGeneration(1704);
if (created.ok) {
local runtime = created.value;
runtime.addLevel(10.0, 25.0, 1.5);
runtime.setRefreshWorkBudget(2);
runtime.updateSource(5.0, 5.0, 1.0, 0.0);
local total = 0;
while (runtime.isRefreshPending()) {
local advanced = runtime.continueGenerationRefresh();
if (!advanced.ok) break;
total += advanced.value.tointeger();
}
if (runtime.getRefreshWorkBudget() == 2 &&
runtime.getCommittedRefreshRevision() == 1 &&
runtime.getPendingGenerateCount() > 0 && total > 0)
result = "ok";
}
)"));
CHECK_EQ(vm.find("result").toString(), std::string("ok"));
}
TEST_CASE("procgen.runtimeGeneration.trimsLowestPriorityCellsDeterministically") {
Procgen proc;
auto runtime = requireRuntime(proc, 18);
runtime->addLevel(10.f, 25.f, 1.5f);
runtime->setMaxGenerating(100);
runtime->updateSource(5.f, 5.f, 0.f, 0.f);
int farX = 0;
int farZ = 0;
PointSet one;
one.add(0.f, 0.f, 0.f);
for (int index = 0; index < 3; ++index) {
auto request = ownRequest(runtime->nextGenerate());
REQUIRE(bool(request));
farX = request->getX();
farZ = request->getZ();
CHECK(runtime->completeGeneration(request.get(), &one));
}
CHECK_EQ(runtime->getResidentPointCount(), 3);
const int trimmedCells = runtime->trimToResidentPoints(2);
CHECK_EQ(trimmedCells, 1);
CHECK_EQ(runtime->getResidentPointCount(), 3);
auto cleanup = ownRequest(runtime->nextCleanup());
REQUIRE(bool(cleanup));
CHECK_EQ(cleanup->getX(), farX);
CHECK_EQ(cleanup->getZ(), farZ);
CHECK(runtime->completeCleanup(cleanup.get()));
CHECK_EQ(runtime->getResidentPointCount(), 2);
}
TEST_CASE("procgen.runtimeGeneration.boundsRetriesAndExplicitlyRecoversFailures") {
RuntimeGeneration runtime(29);
runtime.addLevel(10.f, 6.f, 1.5f);
runtime.setMaxGenerationRetries(1);
CHECK_EQ(runtime.getMaxGenerationRetries(), 1);
runtime.updateSource(5.f, 5.f, 1.f, 0.f);
ProcgenCellRequest* initial = runtime.nextGenerate();
REQUIRE(bool(initial));
CHECK(runtime.failGeneration(initial));
delete initial;
CHECK_EQ(runtime.getFailedCellCount(), 0);
ProcgenCellRequest* retry = runtime.nextGenerate();
REQUIRE(bool(retry));
CHECK(runtime.failGeneration(retry));
delete retry;
CHECK_EQ(runtime.getFailedCellCount(), 1);
CHECK(runtime.debugReport().find("failed=1") != std::string::npos);
const int retriedCells = runtime.retryFailedCells();
CHECK_EQ(retriedCells, 1);
CHECK_EQ(runtime.getFailedCellCount(), 0);
ProcgenCellRequest* recovered = runtime.nextGenerate();
REQUIRE(bool(recovered));
PointSet empty;
CHECK(runtime.completeGeneration(recovered, &empty));
CHECK_EQ(runtime.getActiveCellCount(), 1);
delete recovered;
}
TEST_CASE("procgen.runtimeGeneration.persistsAttributedCellCachesAtomically") {
RuntimeGeneration source(404);
source.addLevel(10.f, 6.f, 1.5f);
source.updateSource(5.f, 5.f, 1.f, 0.f);
ProcgenCellRequest* generated = source.nextGenerate();
REQUIRE(bool(generated));
const int level = generated->getLevel();
const int x = generated->getX();
const int z = generated->getZ();
PointSet points;
const int point = points.add(1.25f, 2.5f, 3.75f);
points.setNormal(point, 0.f, 0.f, 1.f);
points.setRotation(point, 15.f, 45.f, 25.f);
points.setScale(point, 2.f, 3.f, 4.f);
points.setBounds(point, -1.f, -2.f, -3.f, 1.f, 2.f, 3.f);
points.setColor(point, 0.1f, 0.2f, 0.3f, 0.4f);
points.setSteepness(point, 0.65f);
points.setDensity(point, 0.75f);
points.setPointSeed(point, 1234);
points.trySetPointId(point, 18446744073709551614ull).expect("runtime generation test point id");
points.setFloatAttribute(point, "slope", 12.5f);
points.setFloatAttribute(point, "roughness", 0.4f);
points.setIntAttribute(point, "variant", 7);
points.setBoolAttribute(point, "hero", true);
points.setVectorAttribute(point, "wind", 1.f, 2.f, 3.f);
points.setStringAttribute(point, "asset", "oak \"hero\"");
points.setStringAttribute(point, "biome", "forest");
points.add(9.f, 8.f, 7.f);
CHECK(source.completeGeneration(generated, &points));
delete generated;
const std::string persisted = source.serializeCell(level, x, z);
CHECK(persisted.find("EVPCG_CELL 3 404") == 0);
RuntimeGeneration equivalent(404);
equivalent.addLevel(10.f, 6.f, 1.5f);
equivalent.updateSource(5.f, 5.f, 1.f, 0.f);
ProcgenCellRequest* equivalentRequest = equivalent.nextGenerate();
REQUIRE(bool(equivalentRequest));
PointSet equivalentPoints = points;
equivalentPoints.clearPointAttributes(0).expect("runtime generation test metadata reset");
equivalentPoints.setFloatAttribute(0, "roughness", 0.4f);
equivalentPoints.setFloatAttribute(0, "slope", 12.5f);
equivalentPoints.setIntAttribute(0, "variant", 7);
equivalentPoints.setBoolAttribute(0, "hero", true);
equivalentPoints.setVectorAttribute(0, "wind", 1.f, 2.f, 3.f);
equivalentPoints.setStringAttribute(0, "biome", "forest");
equivalentPoints.setStringAttribute(0, "asset", "oak \"hero\"");
CHECK(equivalent.completeGeneration(equivalentRequest, &equivalentPoints));
delete equivalentRequest;
CHECK_EQ(equivalent.serializeCell(level, x, z), persisted);
RuntimeGeneration restored(404);
restored.addLevel(10.f, 6.f, 1.5f);
CHECK(restored.deserializeCell(persisted));
CHECK(restored.hasCell(level, x, z));
CHECK_EQ(restored.getCellRevision(level, x, z), uint64_t(1));
PointSet* loaded = restored.getCellOutput(level, x, z);
REQUIRE(bool(loaded));
CHECK_EQ(loaded->getCount(), 2);
CHECK_EQ(loaded->getX(0), 1.25f);
CHECK_EQ(loaded->getNormalZ(0), 1.f);
CHECK_EQ(loaded->getPitch(0), 15.f);
CHECK_EQ(loaded->getRoll(0), 25.f);
CHECK_EQ(loaded->getScaleY(0), 3.f);
CHECK_EQ(loaded->getBoundsMinY(0), -2.f);
CHECK_EQ(loaded->getColorA(0), 0.4f);
CHECK_EQ(loaded->getSteepness(0), 0.65f);
CHECK_EQ(loaded->getPointId(0), 18446744073709551614ull);
CHECK_EQ(loaded->getFloatAttribute(0, "slope", -1.f), 12.5f);
CHECK_EQ(loaded->getIntAttribute(0, "variant", -1), 7);
CHECK(loaded->getBoolAttribute(0, "hero", false));
CHECK_EQ(loaded->getVectorAttributeZ(0, "wind", -1.f), 3.f);
CHECK_EQ(loaded->getStringAttribute(0, "asset", ""), std::string("oak \"hero\""));
delete loaded;
CHECK(!restored.deserializeCell(persisted + "TRAILING"));
CHECK(restored.hasCell(level, x, z));
RuntimeGeneration wrongWorld(405);
wrongWorld.addLevel(10.f, 6.f, 1.5f);
CHECK(!wrongWorld.deserializeCell(persisted));
RuntimeGeneration constrained(404);
constrained.addLevel(10.f, 6.f, 1.5f);
constrained.setMaxPointsPerCell(1);
CHECK(!constrained.deserializeCell(persisted));
CHECK_EQ(constrained.getRejectedOutputCount(), 1);
RuntimeGeneration legacy(404);
legacy.addLevel(10.f, 6.f, 1.5f);
const std::string legacyV1 =
"EVPCG_CELL 1 404 0 0 0 1 10\nPOINTS 1\n"
"POINT 1 2 3 0 1 0 90 1 1 1 0.5 77\nEND\n";
CHECK(legacy.deserializeCell(legacyV1));
PointSet* legacyPoints = legacy.getCellOutput(0, 0, 0);
REQUIRE(bool(legacyPoints));
CHECK_EQ(legacyPoints->getYaw(0), 90.f);
CHECK_EQ(legacyPoints->getPitch(0), 0.f);
CHECK_EQ(legacyPoints->getColorR(0), 1.f);
CHECK_EQ(legacyPoints->getSteepness(0), 0.5f);
CHECK_EQ(legacyPoints->getPointId(0), uint64_t(0));
delete legacyPoints;
RuntimeGeneration legacyV2Runtime(404);
legacyV2Runtime.addLevel(10.f, 6.f, 1.5f);
const std::string legacyV2 =
"EVPCG_CELL 2 404 0 0 0 1 10\nPOINTS 1\n"
"POINT 1 2 3 0 1 0 10 90 20 1 1 1 0.5 77 0 0 0 0 0 0 1 1 1 1 0.5\nEND\n";
CHECK(legacyV2Runtime.deserializeCell(legacyV2));
PointSet* legacyV2Points = legacyV2Runtime.getCellOutput(0, 0, 0);
REQUIRE(bool(legacyV2Points));
CHECK_EQ(legacyV2Points->getPointId(0), uint64_t(0));
delete legacyV2Points;
}