Four of six challenges conceded; the survey's two main arguments are
replaced rather than clarified.
C1 The survey concluded the sub-100k region was empty without ever
measuring it. It is not empty: tiny-skia fits at 83,956, and the
real windowed floor is fltk at 140,079 — 1.5x corrected headroom,
not 128x. "Two orders of magnitude" is withdrawn.
C1b Following that concession: wgpu+winit is 1,741,979 marginal lines
against a 250,000 target. AM-4a is incompatible with INTENT stage 2
by 7x, and has been since both were written. Raised for the
maintainer; deliberately not decided by this pass.
C2 "Marginal cost zero" was scored on an axis chosen to produce zero,
and the same relocation already flattered sdl2 and fltk in the
survey's own table. One acquisition rule now covers all three, and
it raises two candidates' cost rather than lowering the winner's.
C3 The survey contradicted itself on the second-use rule. cb-render-api
and cb-render-null are withdrawn; cb-render-html ships against the
existing Project trait, and the port waits for stage 2's second use.
C6 The candidate measurements did carry a positive control. The C1
batch did not — it copied dep-weight.py's measurement function
without its guards — and was re-measured under them before being
cited. Figures reproduced unchanged. The HTML row is relabelled
0 by construction, not by measurement.
C4/C5 become six controls binding on ADR-0007.
The recommendation stands. The reasoning that reached it does not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
312 lines
15 KiB
Markdown
312 lines
15 KiB
Markdown
# 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 scripts are not committed, because
|
||
a throwaway probe promoted to a tool is how the second-use rule gets broken
|
||
by accident.
|
||
|
||
**Positive control.** `source_lines()` returns 0 for a crate it cannot
|
||
locate, so a registry path change would report every candidate as costing
|
||
nothing — the direction that flatters the recommendation. Every figure
|
||
below was produced under two guards copied from `dep-weight.py`: an
|
||
unlocatable crate must measure zero, a real one must measure non-zero, and
|
||
any candidate containing an unlocated crate is reported rather than summed.
|
||
No candidate contained one. The review (T02) found that the second batch of
|
||
measurements had copied the measurement function *without* the guards; they
|
||
were re-run under them before being cited here, and reproduced unchanged.
|
||
|
||
**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 by construction** | — |
|
||
|
||
**Revised after review (T02).** The first draft of this section concluded
|
||
from the table above that no windowed toolkit could fit, and called it
|
||
"two orders of magnitude, not a near miss." **That was drawn from a sample
|
||
containing no small windowed toolkits.** Every candidate above is a
|
||
GPU-era stack, because that is what one reaches for; cheap bindings to
|
||
system libraries were structurally absent. Measured, same method, same
|
||
base, under the positive control:
|
||
|
||
| candidate | marginal | vs 92,798 corrected headroom (§3) |
|
||
|---|---:|---:|
|
||
| `tiny-skia` alone (rasterizer, no window) | **83,956** | **fits**, at 90% |
|
||
| `termion` | 133,130 | 1.4× |
|
||
| `fltk` | **140,079** | **1.5×** |
|
||
| `sdl2` | 191,973 | 2.1× |
|
||
| `sdl3` | 276,445 | 3.0× |
|
||
| `raqote` | 286,852 | 3.1× |
|
||
| `minifb` | 774,389 | 8.3× |
|
||
| `crossterm` alone | 802,677 | 8.6× |
|
||
| `softbuffer` alone | 909,416 | 9.8× |
|
||
| `pixels` | 863,528 | 9.3× |
|
||
| `speedy2d` | 1,328,008 | 14.3× |
|
||
|
||
The real floor for a windowed toolkit is `fltk` at **1.5×** the corrected
|
||
headroom, and `tiny-skia` fits outright. **The "two orders of magnitude"
|
||
claim is withdrawn**, and with it the survey's original argument:
|
||
dependency cost alone does not decide this. What replaces it is §4.
|
||
|
||
**The `0` in the first table is by construction, not by measurement** —
|
||
there is no crate to fail to locate. It is relabelled because a zero
|
||
meaning "costs nothing" and a zero meaning "measured nothing" print
|
||
identically, and every other row is a measurement.
|
||
|
||
**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.
|
||
|
||
### The allocation argument, which replaces the affordability one
|
||
|
||
`fltk` at 140,079 is affordable if AM-4a's target moves a little. The
|
||
reason not to buy it is not cost but **allocation**:
|
||
|
||
| | marginal lines |
|
||
|---|---:|
|
||
| corrected headroom (§3) | 92,798 |
|
||
| cheapest windowed stage-1 toolkit (`fltk`) | 140,079 |
|
||
| **`wgpu` + `winit`, which INTENT stage 2 names by name** | **1,741,979** |
|
||
|
||
Stage 2's renderer is measured, unavoidable, named in INTENT, and **twelve
|
||
times** the cost of the stage-1 toolkit it would replace. Spending 1.5× the
|
||
entire remaining budget on a windowing stack that stage 2 discards is the
|
||
wrong allocation regardless of whether the budget can be stretched to
|
||
cover it. That argument does not depend on AM-4a's target being exactly
|
||
250,000 — only on stage 2's bill exceeding stage 1's benefit, which is
|
||
measured and true by a factor of twelve.
|
||
|
||
### AM-4a cannot survive stage 2, and that is not this pass's to decide
|
||
|
||
Following the above honestly produces a finding this survey did not set out
|
||
to look for. Stage 2 exceeds AM-4a's **entire target** by 7× and the
|
||
remaining headroom by 19×. No sequencing, feature-gating, or metric
|
||
correction closes a gap that size: **AM-4a as targeted is incompatible with
|
||
INTENT as written**, and has been since both were written.
|
||
|
||
Either the target moves, or the render port stays outside the AM-4a
|
||
configuration by an argued rule, or stage 2 changes. That is a maintainer's
|
||
decision. It is raised here and **deliberately not resolved** — a pass that
|
||
discovers a budget conflict and also settles it has reviewed nothing.
|
||
|
||
## 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.
|
||
|
||
### The rule that makes "zero" honest (added by review)
|
||
|
||
Scoring the browser at zero is true under AM-4a and suspect in substance: a
|
||
browser is tens of millions of lines of unaudited third-party code. If a
|
||
proposal can satisfy a dependency target by relocating the dependency
|
||
somewhere the target does not look, the target constrains nothing.
|
||
|
||
The same critique convicts `sdl2` and `fltk` above — thin Rust bindings
|
||
whose `.rs` counts exclude the C library that does the work. The metric
|
||
already permits the move; this survey simply never measured a binding
|
||
before, so it never noticed.
|
||
|
||
One rule, applied to all three, belonging in AM-4's definition rather than
|
||
in this pass's reasoning:
|
||
|
||
> **AM-4 counts third-party code the project causes to be acquired.** It
|
||
> does not count runtimes the user already has independently of us — the
|
||
> kernel, the system C library, the shell, the terminal, a web browser. It
|
||
> **does** count a library our build or install instructions cause to be
|
||
> fetched, pinned, or linked, whether or not its source is Rust.
|
||
|
||
Under it: a browser is not counted; **`sdl2` and `fltk` are counted at more
|
||
than their Rust binding, not less**, because they oblige a user to install
|
||
a `-dev` package they would not otherwise have. The rule makes the metric
|
||
*harder* to satisfy by relocation, which is the direction a rule proposed
|
||
by the party it benefits has to run before it is worth anything.
|
||
|
||
**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 (revised after review)
|
||
|
||
The first draft recommended declaring `cb-render-api` and `cb-render-null`
|
||
now, while also saying the interface becomes canonical only at stage 2's
|
||
second use. **Those two cannot both hold**, and INTENT's rule is the one
|
||
that survives: *"No concept becomes canonical merely because it looks
|
||
general. It becomes canonical after surviving a second concrete use."* A
|
||
port written against exactly one implementation that emits whole documents
|
||
will acquire a document's shape — retained mode, full state per update,
|
||
string identity, no frame timing — and stage 2 will find it
|
||
unimplementable. `CommitWindow` is already in this repo with a delete-by
|
||
date for precisely this mistake.
|
||
|
||
1. **No port this pass.** `cb-render-api` and `cb-render-null` are
|
||
withdrawn. A null implementation of an interface that does not exist is
|
||
a null implementation of nothing.
|
||
2. **`cb-render-html`** ships against the **existing `Project` trait** — a
|
||
real interface with real implementations, not a speculative one.
|
||
Marginal AM-4a cost zero, honestly zero under §5's rule.
|
||
3. **The port is declared at stage 2**, derived from two implementations
|
||
instead of imagined from one.
|
||
4. **AM-4a's proc-macro defect is filed separately**, and is not a
|
||
precondition for any of the above.
|
||
5. **AM-4a's incompatibility with stage 2 (§4) goes to the maintainer.**
|
||
|
||
### Six controls the implementation must carry
|
||
|
||
From the review, and binding on ADR-0007:
|
||
|
||
| # | control |
|
||
|---|---|
|
||
| 1 | an unguessable token minted per process, required on every request |
|
||
| 2 | `Origin` / `Sec-Fetch-Site` checked, rejected by default |
|
||
| 3 | listener bound explicitly to `127.0.0.1` |
|
||
| 4 | **a test that a token-less request is refused**, with an M-D1-MUT mutation removing the check and turning it red |
|
||
| 5 | **JavaScript may not construct commands** — the page reports raw pointer facts, Rust decides what command they mean |
|
||
| 6 | **the coverage gate crosses the language boundary** — the HTML counterpart of `every_view_field_is_classified` asserts over the *parsed emitted document*, not over the Rust that emits it |
|
||
|
||
Controls 1–3 without 4 are three claims and no evidence. Control 6 exists
|
||
because asserting over the emitting code would reproduce CB-WP-0011's
|
||
original defect one layer up.
|
||
|
||
### The honest summary
|
||
|
||
Two of this survey's three arguments did not survive review. The budget
|
||
constraint that appeared to force the decision was overstated by 36% by a
|
||
mis-instrumented metric, and *then* overstated again by a candidate list
|
||
that contained no cheap windowed toolkits — the real floor is 1.5× the
|
||
corrected headroom, not 128×. The port the survey proposed to build
|
||
contradicted the rule the same survey cited.
|
||
|
||
What survives is narrower and better founded: stage 1 should not spend
|
||
stage 2's money on a toolkit stage 2 discards, and it should not canonize
|
||
an interface it can only implement once. The recommendation is unchanged.
|
||
The reasoning that reached it has been replaced, which is what the review
|
||
step is for.
|