OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / test / benchmark_cpu_dc_throughput.cpp

127 lines · cpp ref:
// 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;
}