diff --git a/research/CB-RES-0006-render-port.md b/research/CB-RES-0006-render-port.md new file mode 100644 index 0000000..b74e7c1 --- /dev/null +++ b/research/CB-RES-0006-render-port.md @@ -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. diff --git a/workplans/CB-WP-0012-render-port.md b/workplans/CB-WP-0012-render-port.md index 6bb0036..1fc3d77 100644 --- a/workplans/CB-WP-0012-render-port.md +++ b/workplans/CB-WP-0012-render-port.md @@ -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