Skip to content

Latest commit

 

History

History
210 lines (171 loc) · 12.2 KB

README.md

File metadata and controls

210 lines (171 loc) · 12.2 KB

bench_cpp — native bench harness

C++/CUDA bench drivers for the head-to-head decode comparison reported in the STRIDE paper §5.3. Three benchmarks live here:

Source Output Used in
stride_dup_decode_bench.cu stride_dup_decode_bench STRIDE GPU decode throughput (Table 6).
dgf_decode_bench.cu dgf_decode_bench DGF GPU decode throughput (Table 6).
meshopt_shim.cpp meshopt_shim.{dll,so} meshopt CPU decode timings (Table 6).
stride_decode_bench.cu stride_decode_bench Earlier (non-dup) STRIDE variant; kept for reproducibility.

Build requirements

  • CUDA Toolkit 12.x with nvcc on PATH.
  • A host compiler compatible with your CUDA toolkit:
    • Windows: MSVC 2019 or 2022 (cl.exe) via Visual Studio Build Tools.
    • Linux: gcc ≥ 11 or clang ≥ 14.
  • The two third-party submodules populated:
    git submodule update --init --recursive
    cmake -B third_party/DGF-SDK/build -S third_party/DGF-SDK && \
      cmake --build third_party/DGF-SDK/build --config Release
    cmake -B third_party/corto/build -S third_party/corto && \
      cmake --build third_party/corto/build --config Release

Building on Windows

cd bench_cpp
build_dgf_bench.bat
build_meshopt_shim.bat

stride_dup_decode_bench and stride_decode_bench build alongside DGF.

Building on Linux

cd bench_cpp
./build.sh

The script invokes nvcc directly for the CUDA binaries and g++ for the meshopt shim. Adjust CUDA_HOME and CXX at the top of build.sh if your toolchain lives elsewhere.

Running

Each benchmark accepts a list of pre-encoded blobs as input. The Python drivers (scripts/bench_stride_decode_sweep.py, scripts/bench_dgf_decode_sweep.py) encode the eight-mesh corpus first, dump the blobs under bench_cpp/blobs/, and then invoke the native binaries. See the per-script docstring for the exact CLI.

python scripts/bench_stride_decode_sweep.py
# emits   bench_stride_decode_sweep_q12bbox.csv at the repo root
python scripts/bench_dgf_decode_sweep.py
# emits   bench_dgf_decode_sweep.csv

STRIDE-dup CUDA kernel (stride_dup_decode_bench.cu)

One 64-thread block (two warps) decodes one meshlet. For v4_optimized_wind blobs (version 17, LAYOUT 2) warp 0 decodes the bit-plane connectivity (restart / ec / isFirst planes, one lane per triangle, plus the Rice distance stream) while warp 1 reads the residual header and decodes the six bit-plane residual streams; the two warps join, then all 64 threads pack the residuals into walk order, thread 0 runs the predictor chain, and all 64 threads emit vertices and triangles. Bit-plane streams are decoded symbol-parallel: each lane emits one symbol, locating its terminator bit via a popcount scan of the plane words and __fns. Blob versions 4 and 16 (LAYOUT 0/1) decode serial connectivity and their residuals on warp 0.

  • Packed predictor: when every axis uses the canonical parallelogram (pred_n = (1,1,-1), pred_K = 0) and g_bits <= 13, codes and residuals are held as biased 16-bit fields of one u64 and the prediction is one branch-free 64-bit expression on thread 0. When every axis is affine (n0 + n1 + n2 = 2^K, K <= 14) the chain runs planar instead: lanes 0..2 of warp 0 each own one axis as an i32 plane whose residual slot holds (r << K) + h, so a step is three IMADs and a shift ((n0 a + n1 b + n2 c + (r << K) + h) >> K), refs are extracted with one PRMT each. Otherwise the generic predictor runs. The exe prints packed predictor: canonical|affine|off.
  • Static schedule: when the launch grid covers every selected meshlet, block i decodes meshlet i and the work-steal counter is skipped.
  • Build knobs (timing / A/B only): -DPLANE_WORDPAR=1 selects the older word-parallel plane decoder; -DSTAGE_SMEM=1 copies each meshlet's bytes into shared memory before decoding (used only when every meshlet fits 2048 B; slower on large meshes, so off by default); -DSKIP_CONN/RESID/PRED=1 compile a phase out for scripts/bench_phase_split.py (garbage output). Predictor-chain knobs (defaults in parentheses): -DPLANAR_PRED=0|1|2 (1: affine planar, canonical packed on thread 0; 2: canonical planar too); -DPLANE32=0|1 (1: i32 planes with pre-shifted residuals, aliased on the codes/residual pool so shared memory does not grow; 0: legacy i16 planes and a + ((n1 (b-a) + n2 (c-a) + h) >> K)); -DREF_PRMT=0|1 (1: one PRMT per ref; 0: shift + mask + IMAD); -DADDR_TABLE=0|1 (1: pack-pass u16 index table for the refs, +4.5 KB shared memory, measured 1.4x slower from the occupancy loss; 0); -DPRED_UNROLL=n (4).

Golden parity gate — every kernel change must keep decoder output byte-identical:

python scripts/verify_kernel_golden.py [--exe bench_cpp/<exe>]

It dumps every version 20 blob in blobs/stride/ and compares SHA-256 hashes with bench_cpp/golden_hashes.json (the stale dragon / dragon_metis blobs are excluded; the pre-optimisation kernel already faults on them). Re-record with --record only after an intentional output change or after re-dumping blobs. Time two builds against each other with python scripts/bench_kernel_ab.py --a <old.exe> --b <new.exe> --out <csv>: the exes alternate over --rounds (default 30) process invocations of 20 warmup + 100 timed launches each, and the CSV reports the min, median and min-of-min per-launch times with their ratios. Only ratios from one interleaved run are comparable; separate runs drift ~15 % with GPU clock state.

AMD's official DGF HLSL decoder (D3D12 + Vulkan)

shaders/dgf_bench_decode.hlsl includes the SDK's unmodified samples/SampleFramework/shaders/DGFDecompression.hlsl (v1.2.0) and dispatches it the way DGFBVHBuildDecode.hlsl does: 64 threads per 128-byte block, DGFGetTriangle_BitScan_Wave + DGFGetVertex. It differs from the SDK's shader in four ways:

  • Vertex base: read from a prefix table (SRV t1) instead of the optional userData header field.
  • Block id: taken from a selection list (SRV t2), so random subsets of blocks can be decoded.
  • Dispatch shape: 2D grid with a GroupsX root/push constant. D3D12 allows at most 65535 groups per dimension, and tank/dragon have more blocks than that. The selection list is padded with the last id (≤ 2 duplicate blocks).
  • Mask fix: GetTriangleBitsWaveBallot builds its backtrack mask as (1 << numTriangles) - 1. That is 0 for a 64-triangle block, because HLSL shift counts are taken mod 64. The wrapper's copy of that one function uses ~0 >> (64 - numTriangles). Without the fix, 64-triangle blocks that contain a backtrack decode to wrong triangles (e.g. 1,153 on Monkey).

Build (needs VS 2022, Windows SDK, %VULKAN_SDK%; uses the SDK's own DXC):

bench_cpp\build_dgf_hlsl_bench.bat

Run (same CLI and CSV line as dgf_decode_bench.exe):

bench_cpp\dgf_d3d12_decode_bench.exe  bench_cpp\blobs\dgf\xyzrgb_dragon.dgfblob 20 100 --repeats 3
bench_cpp\dgf_vulkan_decode_bench.exe bench_cpp\blobs\dgf\xyzrgb_dragon.dgfblob 20 100 --repeats 3

The shader binaries (dgf_bench_decode.dxil/.rs/.spv) must sit next to the exes. Validate all three DGF decoders (CUDA port, D3D12, Vulkan) against DGFLib: python scripts/verify_dgf_reference.py.

R1 spatial access: explicit units and compact outputs

build_r1_harness.bat builds into r1_build\ (the bench_cpp\*.exe behind committed results are left alone): the STRIDE CUDA harness, the DGF D3D12 harness, the full-decode shader, a compact-output shader (dgf_bench_decode_compact.dxil, -D DGF_COMPACT=1) and r1_spatial_query_test.exe. The Vulkan harness is not changed.

Both harnesses accept (after the positional warmup/runs):

Flag Meaning
--units <ids.u32> little-endian u32 unit ids (STRIDE meshlets / DGF blocks), strictly increasing; an empty file is an empty selection (no kernel launch or dispatch). Duplicates, unsorted or out-of-range ids exit non-zero.
--units-batch <file> records [u32 count][count x u32 ids]; one process runs every selection (warmup, then runs x repeats launches each).
--dump-compact <prefix> per selection k: <prefix>-<k>.units.bin (u32 x 5 per unit: unit_id, vert_offset, vert_count, tri_offset, tri_count), .verts.bin (f32 xyz), .tris.bin (u32 x 3, indices into the compact vertex array: unit vert_offset + local index).
--launch-csv <path> one row per timed launch: selection,repeat,launch,units,verts,tris,empty,submit_us,decode_us,readback_us,total_us,gather_us,upload_us,upload_bytes.
--streamed the compressed file stays in host memory: each launch copies the selected units' bytes (STRIDE: plus their rebased table entries, 40 B each; DGF: the 128-byte blocks) into one pinned/upload buffer, uploads it with one copy and decodes it with the unchanged kernel/shader. Also applies to --query-boxes. Output is byte-identical to the resident path.

Every launch is one complete query: upload the selection and its compact offset tables, decode, copy the compact outputs to host memory, synchronize once. decode_us and readback_us are GPU timers around the kernel/dispatch and the device-to-host copy, upload_us around the host-to-device copy of the launch's upload (upload_bytes; built on the host in gather_us); submit_us and total_us are host timers from launch start to "all work queued" and to "outputs readable in host memory". Output buffers are sized to the largest selection of the process; nothing is cleared or read back beyond the selected units. The STRIDE kernel reads compact offsets by selection slot through a COMPACT template flag; the DGF compact shader reads (vertex base, triangle base) per selection slot. Without these flags both harnesses run the unchanged full-decode path.

r1_spatial_query.h (header-only, no GPU dependencies) loads .stride.bvh / .dgf.bvh sidecars with the Python validation, selects units for a closed box, and filters triangles with the float SAT plus exact integer fallback of scripts/r1_spatial.py. scripts/r1_partial_validate.py checks compact outputs against the full decode and the CPU reference, unit-list rejection, and the header against the Python queries (tests/test_r1_partial.py runs it on fandisk).

Timed spatial queries (r1_query_driver.h)

stride_dup_decode_bench.exe and dgf_d3d12_decode_bench.exe (after the positional warmup/runs) and r1_competitor_query_bench.exe query <draco|corto|meshopt> <file> run the 402-box manifest of scripts/r1_spatial_prep.py end to end:

Flag Meaning
--query-boxes <boxes.f64> 6 x f64 per box in manifest order: 4 per surface position (0.001, 0.01, 0.1, 0.5 of the bbox volume), full-bounds, empty
--sidecar <bvh> .stride.bvh / .dgf.bvh bound to the blob (GPU harnesses; required)
--query-mode independent, growing, warm or all (default)
--query-warmup / --query-launches per box-size cell, default 100 / 300 (round-robin over the cell's boxes)
--query-sequence-repeats growing-mode sequence repeats, default 3 (cache cleared before each)
--query-csv <path> one row per timed launch: mode,query,cell,repeat,launch,selected_units,units_decoded,cache_hits,bytes_accessed,tris_decoded,tris_examined,tris_returned,cache_bytes,lookup_us,submit_us,decode_us,readback_us,cache_us,filter_us,total_us,gather_us,upload_us,upload_bytes
--query-answers <path> per mode, per box: selected unit ids and returned triangle ids of the first timed launch

total_us is the host clock from before the sidecar/BVH lookup to after the exact triangle/box filter; it includes the compact upload, decode, readback, synchronization and cache insertion. GPU timers (decode_us, readback_us) are resolved outside the rows. The competitor harness treats the whole mesh as one unit: independent decodes the file on every launch and scans every triangle; growing/warm retain the decoded mesh plus a triangle BVH (r1_triangle_bvh.h), whose build is charged to cache_us of the first query. Each run ends with one r1_query_summary key=value ... stdout line.

Outputs not committed

Compiled binaries, the blobs/ cache, dump_out/ and golden/ are excluded from the repository via the top-level .gitignore. Re-running the build scripts regenerates them.