// CPU DC meshing throughput benchmark.
//
// Decides terrain-streaming-redesign P1: can a multi-threaded CPU dual-contouring
// mesher beat the current serial-GPU path (~330 chunks/s, 49% lost to per-chunk
// submit+sync+readback)? Measures single-thread ms/chunk and N-thread chunks/sec
// for the exact CPU DC pipeline the workers use (grid fill -> normals -> octree ->
// contour), on a representative streaming workload.
//
// Build:
// g++ -std=c++17 -O3 -fopenmp -I libdc/include test/benchmark_cpu_dc_throughput.cpp \
// libdc/src/dc_contour.cpp libdc/src/dc_octree.cpp libdc/src/dc_qef.cpp \
// libdc/src/dc_density.cpp libdc/src/dc_density_grid.cpp \
// -o test/benchmark_cpu_dc_throughput -lm -lpthread
// Run:
// ./test/benchmark_cpu_dc_throughput [num_chunks]
#include "dc_contour.h"
#include "dc_octree.h"
#include "dc_density.h"
#include "dc_density_grid.h"
#include "dc_pool.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <thread>
#include <atomic>
#include <vector>
using Clock = std::chrono::high_resolution_clock;
static double ms(Clock::time_point a, Clock::time_point b) {
return std::chrono::duration<double, std::milli>(b - a).count();
}
static float density_callback(float x, float y, float z, void* ud) {
return static_cast<DCDensity*>(ud)->evaluate(x, y, z);
}
static DensityConfig make_config() {
DensityConfig c;
c.seed = 42;
c.primary_frequency = 0.02f; c.primary_strength = 1.0f;
c.tunnel_frequency = 0.03f; c.tunnel_strength = 0.8f; c.tunnel_radius = 0.3f;
c.ridge_frequency = 0.015f; c.ridge_strength = 0.5f;
c.warp_frequency = 0.01f; c.warp_strength = 10.0f;
c.ground_bias = 0.1f; c.voxel_size = 1.0f;
return c;
}
// Mesh one chunk through the full CPU DC pipeline. Returns triangle count.
static long mesh_one_chunk(DCDensity& density, int size, float vs, float ox, float oy, float oz) {
float origin[3] = {ox, oy, oz};
DensityGrid grid;
grid.allocate(size, vs, origin);
grid.fill(density_callback, &density);
grid.compute_normals();
int grid_min[3] = {0, 0, 0};
Pool<OctreeNode> node_pool;
node_pool.reserve(size * size * size / 4);
OctreeNode* root = construct_octree_grid_pooled(grid_min, size, grid, node_pool);
DCMeshData mesh;
if (root) dc_generate_mesh_grid(root, vs, origin, grid, 0, mesh);
return (long)mesh.indices.size() / 3;
}
// Representative streaming positions: a walk across a 20x20 chunk grid.
static void chunk_pos(int i, int size, float vs, float* o) {
o[0] = (float)((i % 20) * size) * vs - 320.0f;
o[1] = 0.0f;
o[2] = (float)((i / 20) * size) * vs - 320.0f;
}
int main(int argc, char** argv) {
int M = argc > 1 ? atoi(argv[1]) : 400;
const int size = 32;
const float vs = 1.0f;
const DensityConfig config = make_config();
int ncores = (int)std::thread::hardware_concurrency();
int nthreads = ncores > 2 ? ncores - 2 : 1; // matches dc-terrain clamp(cores-2, 2, 8)
if (nthreads > 8) nthreads = 8;
if (nthreads < 1) nthreads = 1;
printf("=== CPU DC meshing throughput ===\n");
printf("chunks=%d grid=%d^3 cores=%d threads=%d\n\n", M, size, ncores, nthreads);
// Warm up (page in, prime caches)
{ DCDensity d; d.configure(config); float o[3]; chunk_pos(0, size, vs, o);
volatile long t = mesh_one_chunk(d, size, vs, o[0], o[1], o[2]); (void)t; }
// --- single thread ---
{ DCDensity d; d.configure(config);
long tris = 0; auto t0 = Clock::now();
for (int i = 0; i < M; i++) { float o[3]; chunk_pos(i, size, vs, o); tris += mesh_one_chunk(d, size, vs, o[0], o[1], o[2]); }
double t = ms(t0, Clock::now());
printf("single-thread : %.1f ms total | %.3f ms/chunk | %.0f chunks/s (tris=%ld)\n",
t, t / M, M / (t / 1000.0), tris);
}
// --- N threads (each its own density, grid, pool — no shared state) ---
{
std::atomic<int> next{0}; std::atomic<long> tris{0};
auto worker = [&]() {
DCDensity d; d.configure(config);
int i;
while ((i = next.fetch_add(1)) < M) {
float o[3]; chunk_pos(i, size, vs, o);
tris += mesh_one_chunk(d, size, vs, o[0], o[1], o[2]);
}
};
auto t0 = Clock::now();
std::vector<std::thread> ths;
for (int k = 0; k < nthreads; k++) ths.emplace_back(worker);
for (auto& th : ths) th.join();
double t = ms(t0, Clock::now());
printf("%2d threads : %.1f ms total | %.0f chunks/s | %.1fx single-thread\n\n",
nthreads, t, M / (t / 1000.0), (M / (t / 1000.0)) / (M / (ms(t0, t0) + 1)));
printf("=== decision numbers ===\n");
printf(" CPU DC (%d threads) : %.0f chunks/s\n", nthreads, M / (t / 1000.0));
printf(" GPU serial (measured in-game) : ~330 chunks/s (submit .13 + SYNC .85 + readback .75 ms/chunk)\n");
printf(" GPU batched zero-readback (ceiling) : bounded by GPU compute rate; removing the .75ms readback\n");
printf(" and amortizing the .85ms sync => est. ~600-1200 chunks/s\n");
printf("\n If CPU DC (N threads) >= the batched-GPU ceiling, choose CPU: no readback,\n");
printf(" scales with cores, matches godot_voxel/Voxel Plugin. Else prototype batched GPU.\n");
}
return 0;
}