OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / 33a35c9

docs: the CPU twin design, and the measurement that makes it possible
ProcNodesBridge::evaluate answers every CPU density query -- collision, sample_density, the mesher's grid fill -- by matching node TITLES ("Gradient Noise", "Erosion Noise") and running a hand-written pipeline of that shape. Any other graph renders one thing and collides with another. The reasonable prior was that a real graph evaluation would be far too slow for that path. Measured instead, over a chunk-sized 35,937-point fill: graph overhead, no arithmetic 0.1 ms (~3 ns a sample) the same grid with 7-octave erosion 206 ms one point through ProcGeometryNode 15.8 us The 206 ms is the ALGORITHM, which the replica pays too. The machinery is free, so a real evaluator is affordable where the caller can batch -- and the CPU mesher's fill already is a loop over a known grid. ⚠ Two earlier runs of that benchmark reported 0.3 ms and 0.1 ms for graphs whose geometry input was never wired: nothing evaluated, and the number meant nothing. The design records that a density benchmark which does not check how many vertices came back is measuring its harness. Scalar collision queries are the open risk and the design says so: 15.8 us is bake_geometry re-converting the input mesh per call, not the floor, and that floor must be measured BEFORE the replica is deleted rather than after.
33a35c9b58d75da9ed85e31ea4c21054684f272d
by plankatron <fleeky@gmail.com> · 2026-09-05T22:19:22.000Z

Changed files (1) +100 -0

docs/cpu-twin-design.md +100 -0
……@@ -0,0 +1,100 @@
1# The CPU twin: one evaluator, not a replica
2
3Design, 2026-09-05. Nothing here is implemented yet.
4
5## What is wrong today
6
7`DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate` answers every density
8query the CPU makes -- collision, `sample_density`, `get_surface_normal`, the CPU
9mesher's grid fill -- and it does not evaluate the graph. It scans
10`discovered_params_` for node TITLES:
11
12 if (gp.node_title == "Gradient Noise" && gp.socket_name == "Scale") base_freq = ...
13 if (gp.node_title == "Erosion Noise" && gp.socket_name == "Octaves") en_octaves = ...
14
15and then runs a hand-written C++ pipeline of that specific shape. So a graph made
16of any other nodes -- which is every graph the Blender bridge can now author --
17renders one thing and collides with another. The GPU work in
18`glsl-emitter-design.md` fixed the shader; this is the other half.
19
20## The measurements that decide the design
21
22The CPU mesher fills a `DensityGrid` through a callback, one call per corner:
23`dc_worker_cpu.cpp:227`, over `(voxels_per_cell + 1)^3` points. `DCGenerator`'s
24contract is scalar -- `evaluate(x, y, z, lod_voxel_size)` -- and `gradient()`
25calls it four more times.
26
27Measured on this machine, the shipping erosion graph (Gradient Noise -> Erosion
28Noise, 7 octaves) over 35,937 points, CPU path:
29
30| | |
31|---|---|
32| pure graph overhead (geometry in, geometry out, no arithmetic) | **0.1 ms** |
33| the same grid with the erosion filter | **206 ms** |
34| one point, through `ProcGeometryNode` | **15.8 us** |
35
36⚠ The middle row is the ALGORITHM, not the machinery: 0.1 ms over 35,937 points
37is ~3 ns a sample, and the hand-written replica pays the same 206 ms because it
38runs the same `erosion_node_eval_full`. **A real graph evaluator is not the
39expensive thing here.** That is what makes this design possible at all, and it
40was worth measuring rather than assuming -- the reasonable prior was the
41opposite.
42
43⚠ And a warning about the measuring: the first two attempts reported 0.3 ms and
440.1 ms for graphs whose geometry input was never wired, so nothing evaluated and
45the number meant nothing. A density benchmark that does not check how many
46vertices came back measures the harness.
47
48## The shape
49
50**Batch is the primary entry point, because the grid fill already is one.**
51
52 PackedFloat32Array ProcNodeGraph::evaluate_density_batch(const PackedVector3Array &positions)
53
54implemented with the SHIPPING executor over a point cloud of those positions --
55no second arithmetic anywhere -- and the result chosen by exactly the rules the
56GLSL emitter uses:
57
581. an explicit Group Output float, if the graph has one;
592. otherwise the displacement of each point's y, which is what
60 `pos.y - query_y` means in the shader.
61
62One graph execution per grid rather than per sample, which is the same shape the
63GPU has: one dispatch over many points.
64
65**dc-terrain side.** `DCGenerator` gains
66
67 virtual void evaluate_batch(const float *xyz, int n, float *out, float lod);
68
69with a DEFAULT implementation that loops `evaluate()`, so every existing
70generator keeps working untouched. `DCTerrainGeneratorProcNodes` overrides it to
71call proc-nodes once; `dc_worker_cpu`'s fill uses it when present.
72
73**Scalar queries stay scalar.** Collision cannot afford 15.8 us a query -- a few
74hundred per frame is most of a 60 Hz budget. But that number is not the floor:
75it is `ProcGeometryNode::bake_geometry`, which re-converts the input mesh on
76every call. A dedicated entry point holding its one-point geometry resident
77should approach the filter's own cost, which is what the replica pays too. THAT
78NUMBER HAS TO BE MEASURED BEFORE THE REPLICA IS DELETED, and if it cannot be
79reached, the honest outcome is to say so and keep a specialised path with its
80reason written down -- not to ship a collision regression.
81
82## What proves it
83
84`dc-terrain/tests/test_erosion_parity.cpp` already exists for exactly this
85question: it bisects for the surface under the GPU wrapper and under the CPU one
86and reports the difference in METRES. Its first run found 4063 m mean and six of
87sixteen columns with no collidable ground under terrain that rendered. It must
88stay green through this change, and it is the gate that says the twin is gone
89rather than merely rewritten.
90
91Add to it: the same comparison for a graph the replica has never known -- a
92bridged Blender graph with a Group Output float -- which is red today by
93construction, since the CPU answers 0 for anything whose node titles it does not
94recognise.
95
96## What this deletes
97
98`ProcNodesBridge`'s title matching, its hand-written pipeline, and
99`discovered_params_` as a CORRECTNESS mechanism (it stays useful as UI). Roughly
100200 lines whose only job was to be a second, partial copy of the graph.