nigig-org/tools/analysis/README.md
andodeki 087965f17d
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
docs(pdf): does the PDF viewport have a minimal-drawcall strategy? No -- and the renderer is unwired
The same question CAD_DRAWCALL_STRATEGY_ANALYSIS.md asked of the CAD
viewport, asked of pdf-makepad, and prompted by the same datagrid brief:
"virtual viewport on both axes" and "an optimal minimal drawcall
strategy". Those are two techniques, and PDF has a partial version of the
first and none of the second.

The finding that precedes every other one: `PdfRenderer` is exported from
lib.rs and referenced by nothing in the widget's draw path. grep returns
the `pub use` and nothing else. `PdfPageWidget::draw_walk` draws a
background, then a placeholder or link/field affordances -- it never
constructs a renderer and never replays a RenderCommand. Page content
costs zero draw calls because page content is not drawn. That also
explains the `#[allow(dead_code)]` on ClipRect "retained for the
scissor-rect work in Phase 7": clipping is modelled but unreachable.

So the numbers below are projections of what happens the moment the
renderer is wired in, which is exactly when a strategy stops being
theoretical. They are stated as projections in the document.

Makepad's batching model was read from source rather than inferred,
because the CAD note records getting precisely this wrong in its own
first draft (its section 0.1). In draw_vector.rs at the pinned rev,
`cx.new_draw_call` appears exactly twice, both inside `end()`. `begin()`
clears accumulation buffers; `stroke()` and `fill()` tessellate and issue
no draw call. An unbounded number of paints therefore cost one draw call
provided nothing calls `end()` between them.

renderer.rs calls `end()` from `finish_path()`, and `finish_path()` runs
on Save, Restore, PushClip, PopClip and the two Clip ops. Save/Restore
are `q`/`Q`: graphics-state operations, not clip operations, and very
frequent in real files.

Measured by replaying the corpus through that exact state machine --
tools/analysis/pdf_drawcall_census.rs, so the numbers can be reproduced
instead of trusted. 165 pages, 15,185 commands, 3,911 draw calls, 23.7
per page. Of the 2,578 vector draw calls, 2,460 are caused by q/Q and
**two** by clipping. The renderer flushes on the operation that does not
need a flush, and the operation that does need one barely occurs. Colour,
stroke width and the CTM are all baked into vertices on the CPU before
tessellation, so a state change needs no draw-call boundary; only a clip
does, being a GPU scissor concern.

Flushing only on clip change takes the vector side from 2,578 to 166 --
about one per page, 15.5x. The blended figure is a more modest 2.6x and
the document leads with that rather than the flattering one, because text
then dominates: DrawText exposes begin_many_instances, renderer.rs uses
neither it nor begin_deferred_slug_flush, so every run is its own batch,
and the renderer alternates between three DrawText objects which breaks a
batch even when the API is used.

On virtual viewports PDF is genuinely ahead of CAD, and the document says
so: cache.rs is a real LRU with a byte budget rather than an entry count,
generation-tagged, and phase7_exit_criterion.rs asserts the behaviours by
name. That is a tested virtual viewport on the page axis. There is none
within a page -- draw_affordances loops every annotation filtering only
by page index and visibility, never against the viewport rect it already
holds, and nothing skips an offscreen command.

The asymmetry worth recording for anyone porting the datagrid approach: a
grid's virtual viewport is cheap because cell geometry is derivable by
division. A PDF's is expensive because geometry is accumulated through a
stateful CTM, so a command's screen rect is unknowable without
interpreting everything before it. The bbox index is the price of entry
and belongs in RecordingDevice, which already tracks the CTM.

What is not measured is stated plainly: no GPU profiling, no frame times,
because the ui.rs suite that would host a benchmark is still #[ignore]d
on the missing Makepad headless backend. Draw calls are a proxy for cost,
not cost. 24 per page is not alarming on a desktop GPU; the argument is
that the count scales with document complexity rather than viewport size.

No source was changed. The suggested order puts "stop flushing on q/Q"
first because it is a deletion, and puts wiring the renderer third so the
strategy lands with the feature instead of after it.
2026-08-21 05:04:41 +00:00

26 lines
1.1 KiB
Markdown

# Analysis harnesses
One-off measurement programs kept out of the crate tree: they are not
part of any build, and are here so a number in a REVIEWS document can be
reproduced rather than trusted.
## `pdf_drawcall_census.rs`
Backs `REVIEWS/PDF_DRAWCALL_STRATEGY_ANALYSIS.md`. Replays every parseable
page in `crates/apps/pdf/tests/corpus` through the same `finish_path`
state machine as `pdf-makepad/src/renderer.rs`, and counts the draw calls
the renderer would issue.
To run it, drop it into a standalone workspace holding the three engine
crates and invoke it as an example:
W=$(mktemp -d); cd crates/apps/pdf
cp -a pdf-cos pdf-document pdf-graphics tests "$W"/
rm -rf "$W"/pdf-cos/fuzz
printf '[workspace]\nmembers = ["pdf-cos","pdf-document","pdf-graphics"]\nresolver = "2"\n' > "$W"/Cargo.toml
mkdir -p "$W"/pdf-graphics/examples
cp tools/analysis/pdf_drawcall_census.rs "$W"/pdf-graphics/examples/
cd "$W" && cargo run --release -p nigig-pdf-graphics \
--example pdf_drawcall_census -- tests/corpus
It reads only; it changes nothing.