#include "pixelworld/PixelWorld.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { class BenchmarkScheduler final : public eve::pixelworld::PixelWorkScheduler { public: explicit BenchmarkScheduler(std::size_t workerCount) : workerCount_(std::max(1, workerCount)) { workers_.reserve(workerCount_); for (std::size_t index = 0; index < workerCount_; ++index) workers_.emplace_back([this] { workerLoop(); }); } ~BenchmarkScheduler() override { { std::lock_guard lock(mutex_); stopping_ = true; ++generation_; } wake_.notify_all(); } void parallelFor(std::size_t workItems, const std::function& body) override { if (workItems == 0) return; { std::lock_guard lock(mutex_); body_ = &body; workItems_ = workItems; next_.store(0, std::memory_order_relaxed); remaining_ = workerCount_; ++generation_; } wake_.notify_all(); std::unique_lock lock(mutex_); complete_.wait(lock, [this] { return remaining_ == 0; }); body_ = nullptr; } [[nodiscard]] std::size_t workerCount() const noexcept override { return workerCount_; } private: void workerLoop() { std::size_t observedGeneration = 0; for (;;) { const std::function* body = nullptr; std::size_t workItems = 0; { std::unique_lock lock(mutex_); wake_.wait(lock, [this, &observedGeneration] { return stopping_ || generation_ != observedGeneration; }); if (stopping_) return; observedGeneration = generation_; body = body_; workItems = workItems_; } for (;;) { const std::size_t index = next_.fetch_add(1, std::memory_order_relaxed); if (index >= workItems) break; (*body)(index); } { std::lock_guard lock(mutex_); if (--remaining_ == 0) complete_.notify_one(); } } } std::size_t workerCount_ = 1; std::vector workers_; std::mutex mutex_; std::condition_variable wake_; std::condition_variable complete_; const std::function* body_ = nullptr; std::atomic next_ = 0; std::size_t workItems_ = 0; std::size_t remaining_ = 0; std::size_t generation_ = 0; bool stopping_ = false; }; std::size_t parsePositive(char* text, std::size_t fallback) { if (text == nullptr) return fallback; char* end = nullptr; const unsigned long long value = std::strtoull(text, &end, 10); return end != text && *end == '\0' && value > 0 ? std::size_t(value) : fallback; } std::uint64_t hashSnapshot(std::span bytes) noexcept { std::uint64_t hash = 1469598103934665603ULL; for (const std::byte byte : bytes) { hash ^= std::uint8_t(byte); hash *= 1099511628211ULL; } return hash; } std::string_view benchmarkMaterial(std::string_view workload, int x, int y) noexcept { if (workload == "mixed-reaction") return ((x + y) & 1) != 0 ? "fire" : "water"; if (workload == "reaction-only") return ((x + y) & 1) != 0 ? "acid" : "stone"; return "sand"; } } // namespace int main(int argc, char** argv) { using namespace eve::pixelworld; const std::size_t workers = parsePositive(argc > 1 ? argv[1] : nullptr, 4); const std::size_t samples = parsePositive(argc > 2 ? argv[2] : nullptr, 10); const std::string_view workload = argc > 3 ? std::string_view(argv[3]) : "sand"; const bool mixedReaction = workload == "mixed-reaction"; const bool reactionOnly = workload == "reaction-only"; const bool thermalOnly = workload == "thermal-only"; BenchmarkScheduler scheduler(workers); std::vector milliseconds; milliseconds.reserve(samples); std::uint64_t totalVisited = 0; std::uint64_t totalMoved = 0; std::uint64_t totalReactions = 0; std::uint64_t totalThermalTransfers = 0; std::uint64_t totalPhaseChanges = 0; std::uint64_t snapshotHash = 0; for (std::size_t sample = 0; sample < samples; ++sample) { PixelWorld world(7003); for (int y = 0; y < 1024; ++y) for (int x = 0; x < 1024; ++x) { if (thermalOnly) world.setCell(x, y, {MaterialId::Stone, std::int16_t(((x + y) & 1) != 0 ? 700 : 20), 0}); else world.setMaterial(x, y, benchmarkMaterial(workload, x, y)); } const auto started = std::chrono::steady_clock::now(); const StepStats stats = world.advanceScheduled(eve::SimulationTick(1), scheduler) .expect("million-mobile-pixel scheduled tick"); milliseconds.push_back(std::chrono::duration( std::chrono::steady_clock::now() - started).count()); totalVisited += stats.cellsVisited; totalMoved += stats.cellsMoved; totalReactions += stats.reactions; totalThermalTransfers += stats.temperatureTransfers; totalPhaseChanges += stats.phaseChanges; const auto snapshot = world.saveSnapshot().expect("million-mobile-pixel snapshot"); const std::uint64_t sampleHash = hashSnapshot(snapshot); if (sample == 0) snapshotHash = sampleHash; else if (sampleHash != snapshotHash) { std::cerr << "non-deterministic snapshot at sample " << sample << '\n'; return 2; } } std::sort(milliseconds.begin(), milliseconds.end()); const double p50 = milliseconds[milliseconds.size() / 2]; const std::size_t p95Index = std::min(milliseconds.size() - 1, (milliseconds.size() * 95 + 99) / 100 - 1); std::cout << "PIXELWORLD_ACTIVE_BENCHMARK_JSON={\"occupiedPixels\":1048576,\"mobilePixels\":" << (thermalOnly ? 0 : mixedReaction || reactionOnly ? 524288 : 1048576) << ",\"chunks\":256,\"workload\":\"" << (mixedReaction ? "checkerboard-fire-water" : reactionOnly ? "checkerboard-acid-stone" : thermalOnly ? "checkerboard-hot-cold-stone" : "packed-sand-over-empty") << "\",\"samples\":" << samples << ",\"workers\":" << workers << ",\"visited\":" << totalVisited << ",\"moved\":" << totalMoved << ",\"snapshotHash\":" << snapshotHash << ",\"reactions\":" << totalReactions << ",\"thermalTransfers\":" << totalThermalTransfers << ",\"phaseChanges\":" << totalPhaseChanges << ",\"p50Ms\":" << p50 << ",\"p95Ms\":" << milliseconds[p95Index] << "}\n"; return 0; }