diff --git a/tests/testChunkPipeline.cpp b/tests/testChunkPipeline.cpp new file mode 100644 index 0000000..8a27a78 --- /dev/null +++ b/tests/testChunkPipeline.cpp @@ -0,0 +1,154 @@ +#include "testHarness.h" +#include "vf/ChunkPipeline.h" + +#include +#include +#include + +using namespace vf; + +namespace { + +PipelineConfig baseConfig(unsigned threadCount) { + PipelineConfig config; + config.threadCount = threadCount; + config.viewRadius = 2; + config.verticalChunks = 2; + config.gen.seed = 20250904u; + return config; +} + +void primePipeline(ChunkPipeline& pipeline) { + pipeline.setObserver({0, 0, 0}); + pipeline.enqueueViewVolume(); + pipeline.run(); +} + +std::vector digestFor(unsigned threadCount) { + ChunkPipeline pipeline(baseConfig(threadCount)); + primePipeline(pipeline); + return pipeline.digest(); +} + +bool sameBytes(const std::vector& a, const std::vector& b) { + if (a.size() != b.size()) return false; + return std::memcmp(a.data(), b.data(), a.size()) == 0; +} + +bool sameMesh(const Mesh& a, const Mesh& b) { + if (a.vertices.size() != b.vertices.size()) return false; + if (a.indices.size() != b.indices.size()) return false; + if (!a.vertices.empty() && + std::memcmp(a.vertices.data(), b.vertices.data(), a.vertices.size() * sizeof(Vertex)) != 0) + return false; + if (!a.indices.empty() && + std::memcmp(a.indices.data(), b.indices.data(), + a.indices.size() * sizeof(std::uint32_t)) != 0) + return false; + return true; +} + +std::size_t viewVolumeChunkCount(const PipelineConfig& config) { + std::size_t count = 0; + for (int dx = -config.viewRadius; dx <= config.viewRadius; ++dx) + for (int dz = -config.viewRadius; dz <= config.viewRadius; ++dz) + if (dx * dx + dz * dz <= config.viewRadius * config.viewRadius) + count += static_cast(config.verticalChunks); + return count; +} + +} // namespace + +VF_TEST(pipelineTakesEveryQueuedChunkThroughAllFourStages) { + ChunkPipeline pipeline(baseConfig(4)); + std::size_t queued = 0; + pipeline.setObserver({0, 0, 0}); + queued = pipeline.enqueueViewVolume(); + CHECK_EQ(queued, viewVolumeChunkCount(pipeline.settings())); + CHECK_EQ(pipeline.pendingCount(), queued); + + pipeline.run(); + CHECK_EQ(pipeline.pendingCount(), static_cast(0)); + CHECK_EQ(pipeline.results().size(), queued); + for (const ChunkTaskResult& produced : pipeline.results()) { + CHECK(produced.stage == ChunkStage::Ready); + CHECK(!produced.cancelled); + CHECK(produced.entryBytes > 0); + CHECK_EQ(produced.light.sun.size(), + static_cast((chunkSize + 2) * (chunkSize + 2) * (chunkSize + 2))); + } + CHECK_EQ(pipeline.worldView().chunkCount(), queued); +} + +VF_TEST(pipelineResultsComeBackInChunkCoordinateOrder) { + ChunkPipeline pipeline(baseConfig(8)); + primePipeline(pipeline); + const std::vector& produced = pipeline.results(); + CHECK(produced.size() > 1); + for (std::size_t i = 1; i < produced.size(); ++i) { + const Vec3i& previous = produced[i - 1].coord; + const Vec3i& current = produced[i].coord; + bool ascending = previous.x < current.x || + (previous.x == current.x && previous.y < current.y) || + (previous.x == current.x && previous.y == current.y && + previous.z < current.z); + CHECK(ascending); + } +} + +VF_TEST(oneWorkerAndEightWorkersProduceByteIdenticalOutput) { + std::vector serial = digestFor(0); + CHECK(serial.size() > 100000); + CHECK(sameBytes(serial, digestFor(1))); + CHECK(sameBytes(serial, digestFor(4))); + CHECK(sameBytes(serial, digestFor(8))); +} + +VF_TEST(repeatedEightWorkerRunsAgreeWithThemselves) { + std::vector first = digestFor(8); + std::vector second = digestFor(8); + CHECK(sameBytes(first, second)); +} + +VF_TEST(cancelledChunkNeverReachesResultsOrCache) { + ChunkPipeline pipeline(baseConfig(4)); + pipeline.setObserver({0, 0, 0}); + std::size_t queued = pipeline.enqueueViewVolume(); + const Vec3i dropped{1, 0, 0}; + pipeline.cancel(dropped); + CHECK(pipeline.isCancelled(dropped)); + + pipeline.run(); + CHECK_EQ(pipeline.results().size(), queued - 1); + CHECK(pipeline.resultFor(dropped) == nullptr); + CHECK(!pipeline.cache().contains(dropped)); + CHECK(pipeline.worldView().chunkAt(dropped) == nullptr); + CHECK_EQ(pipeline.stats().cancelled, static_cast(1)); + CHECK(pipeline.resultFor({0, 0, 0}) != nullptr); +} + +VF_TEST(chunkOutsideTheViewRadiusIsCancelledBeforeAnyStageRuns) { + ChunkPipeline pipeline(baseConfig(4)); + pipeline.setObserver({0, 0, 0}); + std::size_t queued = pipeline.enqueueViewVolume(); + const Vec3i faraway{9, 0, 9}; + pipeline.enqueue(faraway); + CHECK(!pipeline.inViewVolume(faraway)); + CHECK_EQ(pipeline.pendingCount(), queued + 1); + + pipeline.run(); + CHECK_EQ(pipeline.results().size(), queued); + CHECK(pipeline.resultFor(faraway) == nullptr); + CHECK(pipeline.worldView().chunkAt(faraway) == nullptr); + CHECK(!pipeline.cache().contains(faraway)); + CHECK_EQ(pipeline.stats().cancelled, static_cast(1)); + + PipelineStats counted = pipeline.stats(); + CHECK_EQ(counted.stageCompleted[static_cast(ChunkStage::Generate)], + static_cast(queued)); +} + +VF_TEST(readyChunksLandInTheCachePinnedInsideTheRadius) { + ChunkPipeline pipeline(baseConfig(4)); + primePipeline(pipeline); +