3 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 42b1e7152a |
docs(cad): merge a second render review into the plan, and correct the hash both reviews got wrong
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
A second optimisation plan arrived (frustum culling, geometry merging, GPU instancing, octree/LOD). Rather than run two plans, every claim in it was checked against the tree at |
|||
| f27ace8b7f |
docs(cad): render optimisation plan — and a correction to the analysis it rests on
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Writing the plan meant checking the assumption the analysis rested on,
and the assumption was wrong.
I had called `draw_vector.stroke()` a "tessellation/flush point" and then
reasoned about the 2D path as though each cost a draw call — "~670 flush
points per frame where four would do". Reading
draw/src/shader/draw_vector.rs instead of inferring from a comment:
begin() clears CPU accumulation buffers
stroke() calls tessellate_path_stroke + append_geometry — no draw call
end() the ONLY place cx.new_draw_call appears, twice, both inside it
The whole 2D vector scene is **one draw call**. Makepad's DrawVector is
already a batching design and the CAD code uses it correctly in that
respect. The guard test's wording — "thousands of tessellations per
frame" — was accurate and literal, and I read "draw call" into it.
What survives: no culling anywhere, which was and remains the main
finding; and 3D issuing one draw call per part, which is where draw-call
multiplication is actually real. What changes: batching the 2D loops is
a CPU per-call-overhead win, not a draw-call win, so it drops from
second place to third in the plan and the document says plainly that it
is the small one.
The plan itself, ordered on the corrected facts:
0. Make it measurable. profile_benchmarks.rs has sixteen benchmarks
and none measures frame submission. Check whether Cx already counts
draw calls; if not, add a counting seam. This doubles as the first
test surface viewport_render.rs has ever had — it is 2,083 lines at
0.00% coverage.
1. Cull against the viewport, reusing the cached world AABB that is
already benchmarked at 4.72x and already wired to the mouse-move
path but not the per-frame one.
2. Key part_geoms by ParamHash instead of part id so identical parts
share geometry — the real draw-call win, and the one that matters
for drawings full of repeated columns.
3. Reduce 2D tessellation calls with the queue-then-stroke idiom the
axis grid already uses.
4. LOD, only if the Phase 0 numbers justify it.
Explicitly not doing: a BVH (a linear pass over cached AABBs is
microseconds at this scale) or a render-path rewrite (0% coverage).
One implementation hazard recorded in Phase 1: cull on the drawn extent,
not the model extent. Selection outlines and hover highlights exceed a
part's AABB, and culling on the AABB alone makes them vanish at the
viewport edge.
|
|||
| e6fbcc12da |
docs(cad): analysis — the renderer has no culling and no draw batching
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Asked whether the CAD viewport has the kind of minimal-drawcall strategy
a datagrid needs, and whether virtual-viewport techniques transfer.
Answer: the codebase already knows the technique, applies it in exactly
one function, and does not apply it in the two loops that run every
frame.
`draw_vector.stroke()` is the tessellation/flush point. `queue_dashed_line`
queues segments and issues ONE stroke for the whole axis grid, and a test
in viewport.rs enforces it — "the grid lines should share exactly one
stroke", with a comment warning that stroking per dash is "thousands of
tessellations per frame". The discipline is understood and guarded.
Two loops away from it:
- the base grid strokes once per line (~167 on a 1080p viewport,
where two would do: one for minors, one for majors);
- the parts loop strokes once per part;
- the 3D path issues one draw_mesh.draw() per part, each with its own
geometry buffer and uniforms. No instancing, no state sorting.
And there is no culling at all: grep for cull/frustum/offscreen/in_view
across the 2,461-line renderer returns nothing. Every part is submitted
every frame whether on screen or not.
The sharp part is that the broad-phase already exists.
SceneCache::world_aabb_for is cached by PlacedHash and BENCH_BASELINE.md
records it at 4.72x faster than recomputing. Its only caller is
pick_part — the mouse-move path, which is additionally throttled by
HOVER_PICK_MIN_MOVE_PX. The cheap visibility test is wired to the
occasional path and not to the per-frame one.
The grid, to be fair, IS virtualised properly: visible world bounds plus
20%, with a 1/2/5 nice-number step that adapts to zoom. That is the
datagrid technique done right. It just stops at the grid.
Also recorded: what does not transfer. Widget recycling has no CAD
analogue, and index-range virtualisation does not either — CAD is
continuous space, so it needs a spatial test rather than a row range. At
500 parts a linear pass over cached AABBs is microseconds; a BVH only
earns its complexity somewhere past ~50k parts and nothing suggests that
is the target.
Caveat stated in the document: no profiling was run, and there is no
frame-submission benchmark in profile_benchmarks.rs — its sixteen
benchmarks all measure CPU work. The structural claims are read off the
code and are solid; the consequence in dropped frames is not measured,
and measuring it needs a live Cx.
|