CB-WP-0012-T01: survey the render port — and find AM-4a mis-instrumented

Measured with dep-weight.py's own method so the numbers subtract.
Marginal cost against the 23-crate base graph, not headline size:

  egui+eframe          2,782,849   742x headroom
  wgpu+winit           1,741,979   465x
  softbuffer+tiny-skia 1,147,081   306x
  ratatui+crossterm    1,067,013   285x
  macroquad              480,501   128x
  HTML/SVG emission            0     0x

ratatui costing more than macroquad is the surprise: rustix pulls
linux-raw-sys at 479,901 lines.

Separately, AM-4a counts proc-macro crates that never reach a binary:
89,048 lines, 36.2% of the shipped-runtime figure, syn alone 66,916.
Real headroom is 92,798, not the 3,750 this repo has been citing.

The recommendation is unchanged by that correction — macroquad is 5.2x
over even at 92,798 — which is the only condition under which a survey
may propose correcting a number in its own favour. The metric fix is
filed separately, not bundled with the decision it unblocks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-02 04:05:52 +02:00
parent 331e7e9044
commit 53abbaf68f
2 changed files with 208 additions and 1 deletions

View file

@ -0,0 +1,183 @@
# CB-RES-0006 — the render port, and what a 2D table costs
CB-WP-0012 T01. Step 1 of the inner loop. Measured 2026-08-02 at
`331e7e9`.
The question: INTENT stage 1 needs `cb-render-api` and a first real
implementation. AM-4a reports **3,750 lines of headroom** (246,250 of a
250,000 target). Is that a real obstacle, or an artifact of the
instrument?
Both, and not in the proportions I expected.
---
## 1. Method
Every figure below comes from the same method `tools/dep-weight.py` uses
for AM-4a: resolve a real dependency graph with
`cargo tree --edges normal --prefix none`, drop path dependencies, and
count `\n` in every `.rs` file of the vendored source. Reusing the method
verbatim is the point — a survey that measures candidates one way and the
budget another produces two numbers that cannot be subtracted.
Probe crates were built in a scratch directory, one per candidate, and
resolved against the live registry. The script is
`$CLAUDE_JOB_DIR/tmp/survey/measure.py`; it is not committed, because a
throwaway probe promoted to a tool is how the second-use rule gets broken
by accident.
**Marginal, not total.** A candidate's headline size overstates its cost
by whatever it already shares with the base graph. What AM-4a charges is
the delta.
## 2. What a 2D table costs
Base: `games-ground --no-default-features`, 23 third-party crates,
246,250 lines.
| candidate | total | shared with base | **marginal** | vs 3,750 headroom |
|---|---:|---:|---:|---:|
| `egui` + `eframe` | 2,946,121 | 163,272 | **2,782,849** | 742× |
| `wgpu` + `winit` | 1,866,820 | 124,841 | **1,741,979** | 465× |
| `softbuffer` + `tiny-skia` + `winit` | 1,271,922 | 124,841 | **1,147,081** | 306× |
| `ratatui` + `crossterm` | 1,146,363 | 79,350 | **1,067,013** | 285× |
| `egui` alone (no windowing) | 778,067 | 110,542 | **667,525** | 178× |
| `macroquad` | 480,729 | 228 | **480,501** | 128× |
| HTML/SVG emission, no Rust toolkit | — | — | **0** | 0× |
The cheapest candidate that opens a window costs **128 times** the entire
remaining budget. This is not a near miss to be negotiated; it is two
orders of magnitude.
**A surprise worth recording:** `ratatui` — a *terminal* library, the
option one reaches for expecting it to be the cheap one — costs more than
`macroquad`, a 2D game framework with a GPU backend. The reason is
`linux-raw-sys` at 479,901 lines and `libc` at 129,990, pulled through
`rustix`. Neither is code anyone audits by hand; both are largely
generated constant and type definitions. Which raises the next question.
## 3. The instrument is wrong, and has been all along
AM-4a counts the `--edges normal` graph. That graph includes **proc-macro
crates**, which run in the compiler and never reach a shipped binary.
| `games-ground --no-default-features` | crates | lines |
|---|---:|---:|
| as AM-4a measures it | 23 | **246,250** |
| excluding proc-macro crates (`--edges normal,no-proc-macro`) | 18 | **157,202** |
| the difference | 5 | **89,048** (36.2%) |
The five are `syn`, `quote`, `proc-macro2`, `unicode-ident` and
`serde_derive`. `syn` alone is 66,916 lines — the single largest entry in
the shipped-runtime figure, and it is a parser for Rust source that exists
only at build time.
**So 36.2% of what AM-4a calls "what a game ships" is not shipped.** Under
a corrected metric the headroom is not 3,750 but **92,798**. Every pass
that has cited the 3,750 figure — including CB-WP-0011's own reasoning for
deferring this declaration — cited a number that was wrong in the
conservative direction.
**This does not change the recommendation, and that is the point of
checking it before recommending.** Under the corrected metric `macroquad`
still costs 5.2× the available headroom, and every other windowed
candidate is worse. The dependency argument survives the correction of the
number it rests on, which is the only condition under which a survey may
propose correcting a number in its own favour.
The correction is proposed as a separate, narrow change — the metric
should measure what ships — and it must not be bundled with the render
decision, because a budget correction that arrives attached to the request
it unblocks is indistinguishable from motivated reasoning even when it is
right.
## 4. What stage 1 actually asks for
> *card/token/hand/relationship-graph visualization, drag-to-propose,
> debug inspector, hot-seat play*
Read plainly: draw a table, let a human point at it, and let several
humans take turns at one machine. It does **not** ask for a GPU, a frame
loop, physics, or 60fps. Those are stage 2 (*"Physical 3D tabletop —
wgpu renderer, Rapier-backed physics"*), and stage 2 is where the 1.7M
lines of `wgpu` + `winit` get argued for on their merits.
Buying a GPU toolkit now would mean paying stage 2's dependency bill to
satisfy stage 1's requirements, one stage early, against a budget that
cannot cover it, and before the port interface has ever had a second
implementation to be validated against.
## 5. The option that is not a Rust toolkit
Emit HTML with inline SVG and inline JavaScript; open it in the browser
the machine already has.
- **visualization** — SVG draws cards, tokens and a relationship graph
natively, and the relationship graph is the one element every Rust 2D
toolkit would have made us hand-roll anyway.
- **drag-to-propose** — pointer events in the emitted page.
- **debug inspector** — already shipped (CB-WP-0011), and its renderer is
already a total function from a projection to text; HTML is a second
output format for the same walk.
- **hot-seat play** — one browser tab, seats taking turns, the same
commit/reveal the CLI player uses.
**Marginal AM-4a cost: zero.** Not "small" — zero. Emitting HTML is string
formatting; the 2,897-line `svg` crate measured above is not needed and is
not proposed. The input path costs no dependency either: a loopback
HTTP/1.1 listener sufficient for `GET /` and `POST /command` is
`std::net::TcpListener` plus roughly 250 lines of our own code, and our
own code is not what AM-4a governs.
This is the central rule applied literally — *own the semantics; assimilate
the implementation*. The browser is the most thoroughly assimilated 2D
renderer available, and it is the one implementation we are guaranteed not
to have to vendor, audit, or keep on a target.
**Own-code cost is a real cost.** ~250 lines of hand-rolled HTTP is code
we own, test and carry, and hand-rolled HTTP has a bad security history.
It listens on loopback only and speaks to a page it emitted itself, which
bounds the exposure but does not erase it. That trade — 250 own lines
against 480,501 third-party ones — is the decision ADR-0007 has to make
explicitly rather than inherit from this survey.
## 6. What this cannot do
Stated concretely, because the adversarial review (T02) should not have to
discover it:
- **No frame loop.** Animation, drag *feedback* at pointer-event rates,
and anything requiring sustained redraw are the browser's problem, not
the port's — the Rust side emits state and receives commands. If stage 1
later wants tweened card motion, it is written in the emitted JS, in a
language this repo otherwise does not use and has no gates for.
- **No offline binary.** A game that "ships" ships a process that wants a
browser. For a headless-first rules engine that is nearly free; for a
consumer product at stage 4 it is not.
- **A second language enters the build.** JavaScript in emitted strings is
untested by `cargo test` and invisible to every gate this repo has.
This is the strongest argument against, and the one T02 should press.
- **It is not stage 2's renderer.** Nothing here carries forward to wgpu
except the port interface — which is the argument *for* doing it, under
the second-use rule, but only if the interface is written to be
implemented twice rather than written around HTML.
## 7. Recommendation
1. **`cb-render-api`** — a canonical port over the existing `Project`ion:
state in, drawable primitives out, proposed commands back. No
assimilated types cross it (ArchitectureBlueprint §Never leak).
2. **`cb-render-null`** — the reference no-op, so tests and agents need
neither GPU nor browser.
3. **`cb-render-html`** — the first real implementation, marginal AM-4a
cost zero.
4. **`cb-render-wgpu` is stage 2's**, and is where the port interface gets
its second use and only then becomes canonical.
5. **AM-4a's proc-macro defect is filed separately** and is not a
precondition for any of the above.
**The honest summary:** the constraint that looked like it would force
this decision turns out to be overstated by 36%, and the decision does not
change. The recommendation is what it would have been at 92,798 lines of
headroom, which is the only reason it is worth trusting at 3,750.