graph3d — rotate/spin performance: gatling projection + the GPU cloud paint lane — design
Class:
design· Function axis:f0the observed single-core rotation wall ·f1the rotate/spin decomposition bench + Loki numbers ·f2the gatling projection lane (project_all) ·f3the LIVE instanced GPU cloud paint (graph_gpu::cloud) ·f4the RED-when-broken tests. Strict: every claim points at a realfile:line. rayon is FORBIDDEN — gatling only.
f0 — the wall Rickard saw
Rotating/navigating a 30 000-node 3-D graph in korp pegged one CPU core. The graph layout is already multicore + frozen (converge-once-then-freeze, see force-layout-twolane-design.md) — it does not recompute during rotation. What runs every frame while the camera spins is the per-node projection + the paint of the node/edge cloud. The prompt's hypothesis was that the projection (Graph3D::project_all, facett-graph3d/src/lib.rs) was the single-core cost.
The bench disproved that. examples/rotate_bench.rs decomposes the per-frame cost — (a) projection, (b) depth sort, (c) egui paint/tessellation, (d) total — and measures CPU cores busy from /proc/self/stat. The projection is trivially cheap; the egui CPU tessellation of 30k discs + edges is the ~99 % single-core wall. Measure first, then fix the dominant cost — not the assumed one.
f1 — the rotate bench + Loki numbers
facett-graph3d/examples/rotate_bench.rs spins the camera 120 frames for n ∈ {3k, 30k, 100k}. Loki (Ryzen 9 7900, 12c/24t, 64GB, RTX 5080), --release --features wgpu:
n proj serial proj gatling speedup ser cores par cores sort ms paint ms total ms/f tris/f
3000 0.002 ms 0.211 ms 0.01x 0.0 3.2 0.055 3.337 3.603 246 880
30000 0.020 ms 1.040 ms 0.02x 0.0 9.4 0.615 33.751 35.405 2 467 528
100000 0.058 ms 3.172 ms 0.02x 1.4 10.2 2.584 112.418 118.173 8 224 016
Read it:
- Projection is NOT the wall. Serial projection is 0.02 ms/frame at 30k (0.06 ms at 100k). It never shows up. Fanning it across cores via gatling adds ~1 ms of fork-join dispatch (the
proj gatlingcolumn) — a pessimization at these sizes, because the per-node work (one Y-rotation) is memory-cheap.par coresproves the fan DOES saturate cores (9.4 at 30k) — it's just aimed at the wrong cost. - PAINT is the wall. egui tessellates 2.47 M triangles at 30k (8.2 M at 100k) on the UI thread — 33.75 ms/frame at 30k (~112 ms at 100k). That is the pegged core: at ~30 fps a 33 ms single-thread frame is one core maxed. Even with
egui_wgputhe GPU only rasterises the meshes — the per-shape tessellation is CPU, single-thread.
f2 — the gatling projection lane (project_all)
Graph3D::project_all (facett-graph3d/src/lib.rs) fans the per-node projection across every core through znippy's gatling_for_each above PROJ_PARALLEL_MIN (2048 nodes), serial below. The kernel is the shared free fn project_pt used by BOTH the serial Graph3D::project and the gatling lane, so the output is byte-identical to the old .iter().map().collect() (index-order preserved, no accumulation → FC-9 determinism intact, every snapshot golden byte-green). gpu_scene/gpu_scene_at and the FC-9 view projection both route through it (L5 — one projection seam). It is honest multicore projection, but the bench shows it is not where the frame goes — kept because it is the requested "projection across all cores" and is deterministic + free of golden risk, not because it moves the rotation wall.
f3 — the LIVE instanced GPU cloud paint (graph_gpu::cloud) — the real fix
The instanced GPU lane (graph_gpu::GraphGpu, node/edge instanced pipelines + bloom) already existed but was offscreen/readback only (test-proof path). Its own header called wiring it into an egui_wgpu callback "the follow-up". This is that follow-up.
facett-graph3d/src/graph_gpu.rs module cloud:
CloudRenderer— the persistent resources: the reusedGraphGpupipelines + a sharedfacett_core::render::gpu::OffscreenColorDepth(HDR colour target + blit) + the per-frame instance buffers (kept on the renderer so they outlive the frame's submit).CloudPaintCallback(egui_wgpu::CallbackTrait) —prepareuploads the projectedGraphSceneinstance buffers and recordsGraphGpu::encode_scene(the SAME additive node/edge instanced draw the headlessrenderuses — L5, no second renderer) into the offscreen HDR colour view;paintblits it into egui's pass, scissored to the widget.install_cloud_renderer(&RenderState)— host installs it once (mirrorslogo3d::gpu::install_renderer/ map3d).
Wiring into the FC-9 view (facett-graph3d/src/lib.rs): Graph3D::with_gpu(true) opt-in
Graph3D::draw_gpu_cloudpushes the callback for the pane (projected at the exactrender_yawviagpu_scene_at) and returnstrue, soviewskips the two heavy CPU paint loops (the 30k edge + node tessellation). Labels / shockwave / travels stay as thin CPU overlays on top. Defaultuse_gpu = false⇒ the CPU painter draws — every existing snapshot golden (neon / rabbit3d / travels / demo System-Map) is byte-unchanged; the GPU path is opt-in, tested for non-blank equivalence, not byte-equality.
Net: with with_gpu(true) + the renderer installed, rotating a 30k cloud issues two instanced draw calls on the RTX 5080 instead of tessellating 2.47 M triangles on one core — the 33 ms/frame single-core wall leaves the CPU frame entirely.
f4 — RED-when-broken
-
gatling_projection_is_bit_identical_to_serial(src/lib.rstests) — the gatlingproject_all== the serial reference bit-for-bit (f32::to_bits) at every size straddlingPROJ_PARALLEL_MIN(incl. 5k/30k in the fan-out), 5 runs each, 5 yaws. A worker-order race or non-index-order write turns it red. -
gpu_cloud_renders_nonblank_via_instanced_callback(tests/gpu_cloud.rs) — installs the realCloudRenderer, opts a 4k graph intowith_gpu, drives the live egui_kittest wgpu backend, asserts the blitted frame is non-blank (the instanced draw- offscreen blit works end to end). Degrades to a skip with no adapter, never a false fail.
-
All prior graph3d goldens (neon/rabbit3d/travels snapshots, the CodeVault/System-Map determinism paths) stay green — the GPU lane is opt-in-off by default.
korp
korp enables facett-graph3d/wgpu. To rotate multicore+GPU it must (1) call Graph3D::with_gpu(true) on its 3-D graph and (2) graph_gpu::cloud::install_cloud_renderer once with the eframe wgpu_render_state (like the demo installs the map renderers, facett-demo/src/main.rs:45). With both, the 30k rotation draws on the 5080 and the single-core tessellation wall is gone; projection also fans across cores (small, per the bench). Without the opt-in it renders exactly as today.