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.

View file

@ -46,7 +46,7 @@ obstacle or an artifact of how AM-4a is instrumented.
```task
id: CB-WP-0012-T01
status: todo
status: done
priority: high
```
@ -74,6 +74,30 @@ that has been true for every pass that has cited it. Check this before
recommending any budget change — a survey that argues for raising a target
it has not first audited is arguing in its own favour.
**Done 2026-08-02.** [CB-RES-0006](../research/CB-RES-0006-render-port.md).
Two findings, and they point opposite ways.
The cheapest candidate that opens a window (`macroquad`) costs a marginal
**480,501** lines — **128×** the headroom. `egui` + `eframe` costs
2,782,849. `ratatui`, the option one expects to be cheap, costs *more than
macroquad* (1,067,013) because `rustix` pulls `linux-raw-sys` at 479,901
lines.
And AM-4a is mis-instrumented: **36.2% of the shipped-runtime figure
(89,048 lines) is proc-macro crates** — `syn` alone is 66,916 — which run
in the compiler and never reach a binary. Real headroom is **92,798**, not
3,750. Every pass that cited 3,750, this workplan's own Purpose included,
cited a number wrong in the conservative direction.
The recommendation survives the correction: `macroquad` is still 5.2× over
at 92,798. That was the condition for proposing the correction at all, and
the correction is filed separately rather than bundled with the decision
it would unblock.
Recommended: `cb-render-api` + `cb-render-null` + **`cb-render-html`** —
emitted HTML/SVG/JS, marginal AM-4a cost **zero**, with `cb-render-wgpu`
left to stage 2 where it becomes the interface's second use.
## Task: adversarial review of the survey
```task