#include "procgen/Procgen.h" #include "procgen/RuntimeGeneration.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include #include #include 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 view() const noexcept { return owner_ ? owner_->resolveRuntimeGeneration(handle_) : eve::script::Borrowed(); } /** @brief Preserves the concise `runtime->operation()` test syntax. */ [[nodiscard]] eve::script::Borrowed 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; [[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(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 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 empty; auto emptyResult = runtime->completeCleanupsAtomic(empty); REQUIRE(!emptyResult.ok()); CHECK(runtime->isRequestCurrent(firstCleanup.get())); CHECK(runtime->isRequestCurrent(secondCleanup.get())); std::vector 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 wrongOwner{firstCleanup.get()}; auto wrongOwnerResult = otherRuntime->completeCleanupsAtomic(wrongOwner); REQUIRE(!wrongOwnerResult.ok()); CHECK(runtime->isRequestCurrent(firstCleanup.get())); std::vector 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(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(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(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(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> 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; }