RUST / WGPU · MEASURED IMPLEMENTATION
Move the work. Measure the whole frame.
The GPU now decides which Gaussians to draw, sorts their depths, and supplies the draw count. The CPU still manages streaming, the camera and command encoding.
Open the live CPU / GPU comparison →What the measurements say
Same checkpoint, 1280 × 960, three calibrated cameras and four times including both endpoints. Two warmup rounds, seven measured rounds, alternating backend order; 84 frames per backend per scene. CPU baseline is the previously optimized v9 path.
Frame time here is serialized CPU preparation → GPU completion, with a fixed small timestamp copy. It excludes correctness readbacks, networking, display refresh and frame pipelining. It is not browser FPS. GPU total includes all culling and sorting passes, not just rasterization.
Native summary JSON · Browser A/B and playback evidenceFollow one Gaussian through the GPU
- Evaluate and cull. Read the resident 128-byte STG record. Evaluate cubic motion and temporal opacity at t. Apply the same 1/255 opacity cutoff and center-frustum margin as the CPU. Invisible records receive a sentinel key.
- Sort a full 32-bit depth key. Eight stable 4-bit radix passes: count each digit per workgroup, prefix-scan the counts, then scatter records to their destinations. Shared bit masks preserve input order within each bucket. Equal computed depths retain ascending source IDs.
- Compact by sorting, then draw indirectly. Sentinels move behind all visible entries. A GPU atomic counter supplies instanceCount; draw_indirect consumes it without a CPU round trip. The final sort pass writes the packed 4-byte source-ID vertex stream.
- Keep the same rasterizer. The existing vertex shader projects each Gaussian to an ellipse; the fragment shader evaluates its falloff and blends it. Source replacement invalidates GPU preparation. Unchanged frames reuse the prepared order.
What is preserved, and what is not promised
- No depth quantization, reduced point count or lower resolution. Every source ID is checked for loss/duplication, GPU keys for sorted order, and exact-depth ties for stability.
- CPU and GPU floating-point evaluation need not be bit-identical. In one Flames case, GPU exp rounding retains one extra Gaussian at the 1/255 cutoff (ID 324748); that frame's maximum channel difference is 1/255. The report records the ID, CPU opacity and a 16-ULP threshold-band check. Other visible-set differences fail validation. Nearly equal computed depths can also swap order: full-f32 sorting does not imply identical CPU/GPU arithmetic.
- This retains the existing center-frustum heuristic; it is not ellipse-extent culling or occlusion culling. Sorting still touches every resident record, including invisible sentinels. Extra GPU scratch is about 21 bytes per resident Gaussian, plus small uniforms.
- Core WebGPU compute only: no subgroup requirement or NVIDIA-only synchronization. Measured hardware is an RTX 4090 on Windows/DX12. Metal, integrated GPUs and mobile still need their own performance and correctness validation.
- A 16-byte asynchronous count readback serves the UI only; drawing never waits for it. Optional timestamp queries measure GPU work. Streaming remains a separate worker/R2 pipeline; these are short checkpoints, not a trained 200-second model.
Use it in your engine
let mut splats = StgPass::new_gpu(&device, &ply, format, depth)?; splats.prepare(&queue, t, view, projection, viewport)?; splats.encode_prepare(&queue, &mut encoder, None); // Begin the host render pass AFTER compute preparation. splats.draw(&mut pass); // GPU-generated instance count // Submit before preparing the next frame with these buffers.
Rust orchestration · Culling kernel · Stable radix kernels · Reproduce the A/B