iris: a Rect is not size-independent, and P1a's block appearance verified

The defect P1a's screenshots found, and the one that mattered:
`Rect::is_size_independent()` answered `true`. A `Rect` fills whatever
region it is handed, so its content *is* the region -- and
`draw_inner`'s fast path, which rewrites a widget's primitives with
`r.outside(&from).within(&region)` instead of redrawing it, cannot
reproduce that once a region carries both `rel` and `abs`. What it
looked like: a fenced code block's background kept the height of the
provisional full-region draw `Span` does in its first phase, so one
fence's panel covered every block below it and every row below that,
with the text underneath laid out correctly. Likely the same cause as
RUST.md's older "the composer bar's grey background is not drawn".

Also here: a quote's bar is a `Stack` background behind padded text
rather than a two-child `Span(Dir::RIGHT)` (one widget fewer and no
provisional pass), and `transcript-ui`'s `transcript` example gains a
row holding one of every block kind -- the fixture's own heading,
paragraph, fence and table source, plus a list and a quote, which the
fixture has neither of.

docs/bench/p1a-2026-09-06/ has the pairs and docs/RUST.md's P1a box
names what still differs. The iris half is from the desktop backend
because this emulator cannot draw iris's glyphs at all (solid boxes,
reproduced on the previous commit, with Compose drawing text correctly
on the same AVD); both routes to Vulkan on this AVD were tried and both
fail. Bench stream phase, assertions live, no abort: p50 53.0ms p90
108.6ms p99 132.0ms against 52.8/108.1/137.3 before -- unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-06 19:30:39 -04:00
1 parent 64f64b54e5
commit 69525bd131
11 files changed
+355 -34

No files matched your search

+135 -11
View File
@@ -5465,17 +5465,141 @@ device.
comparison fair. **Sub-order, decided by the design agent, by what
the bench fixture exercises and Compose already draws** (each is
one agent; tick and date in place):
- [ ] **P1a — markdown block rendering parity.** Now that a row is
a column of per-block widgets (`e1030d6`), draw each block
the way Compose does: headings at their sizes, fences in a
mono face with `client-core`'s `highlight` spans and a
distinct background, bullet/numbered lists with indent,
tables as aligned columns, block quotes, links as tappable
spans (opening through the platform — the "tappable link"
primitive in IRIS_TODO's "Build (for the port)"). Against
`app/bench-fixture/`, screenshot beside the Compose bench
build on the same emulator. Pure parts (block → style
mapping) tested in `client-core`/`transcript-ui`.
- [x] **P1a — markdown block rendering parity.** Done 2026-09-06.
Each top-level block is drawn in one of **three frames**
(`transcript-ui::markdown::BlockFrame`, mapped from
`BlockKind` by the pure `frame_of`): `Plain` (a paragraph,
heading, list or rule -- text and spans, no extra widget),
`Verbatim { fill }` (a fence or a table -- a rounded panel
that does not wrap and pans sideways, `CodeFence.kt`'s
`horizontalScroll`), and `Quote` (a bar behind text padded
past it). Everything else markdown can say is expressed in
`SpanStyle`s, which cost no widgets.
**What each block looks like now, against `Markdown.kt`:**
- *Headings* -- Material's own ladder, the six sizes
`markdownTypography` picks (24/22/16/14/12/11 at a 16pt
body), bold. Was a three-step 28/24/21/19.
- *Fences* -- monospace on Mocha Crust, rounded, with
`client_core::highlight`'s spans by language in the same
Catppuccin palette `Theme.kt`'s `catppuccinSyntax()` uses.
An unknown language is plain rather than coloured by the
nearest one. A fence being streamed into re-renders only
the last block (`RowBlocks::apply_delta`), so earlier
fences are never re-scanned.
- *Lists* -- the bullet ladder `MarkdownPieces.kt` draws
(disc/ring/square by depth) and ordered lists counting from
the number written, markers in Lavender.
- *Tables* -- padded monospace columns measured from the
cells, header bold, a rule under it, on Surface 0. A real
grid was rejected; docs/DECISIONS.md, 2026-09-06, has why.
- *Quotes* -- a Surface 2 bar down the left, text one shade
back from body.
- *Links* -- coloured and underlined as before, and now
**tappable**: `GestureOutcome::Tapped` (a press that
committed to neither a pan nor a selection),
`TextEditCtx::byte_at` for which byte, and
`iris::platform::OpenUrl` for the platform (`xdg-open`/
`open`/`start`; on Android an `ACTION_VIEW` intent deferred
to `after_input`, the shape `pending_show_keyboard` uses).
**Screenshots: `docs/bench/p1a-2026-09-06/`.**
`compose-heading-fence-table.png` and
`compose-fence-table.png` are the Compose `bench` build on
this checkout's AVD against `app/bench-fixture/`;
`iris-blocks.png` is iris rendering the same heading,
paragraph, link, fence and table source (plus a list and a
quote, which the fixture has neither of) from
`transcript-ui`'s own `transcript` example.
**Why the iris half is not from the emulator**, which the
pass condition asked for: **the emulator cannot draw iris's
glyphs at all.** Every character comes out as a solid filled
box of the right width -- `iris-emulator-gles-glyphs.png`.
Established as *not* this change's doing and not the app's:
the previous commit (`20303e0`) draws the same boxes, and the
Compose bench build on the same AVD in the same minute draws
text perfectly. The atlas sample's alpha reads as 1 under
`-gpu host` + `-feature -Vulkan` (Mesa 26.2.2 / virgl), which
is what an *incomplete* GL texture returns (0,0,0,1). Both
ways out were tried and both fail: `GPU_HOST_FEATURES=" "`
still dies at boot with gfxstream's documented "Format
VK_FORMAT_R8G8B8A8_UNORM is not supported ... Failed to find
memory type for ColorBuffers", and `EMU_GPU=software` does
give the guest SwiftShader Vulkan but iris **SIGSEGVs inside
`surface_changed`** on it. So the appearance half of this box
is taken on the desktop/winit backend, which renders on the
host's real GPU through `iris/run-headless.sh`.
**What still differs, pair by pair:**
1. *Colour, on the desktop shot only.* The winit surface is
sRGB and the shader writes the palette's bytes as linear,
so every fill reads ~4x lighter: Crust (17,17,27) comes out
(73,73,91), measured. Not a palette error and not present
on Android, where the previous pass measured the composer
bar at rgb(41,40,49) for a declared (40,40,46). Worth its
own item; it makes the desktop build a poor colour
reference until fixed.
2. *A list's wrapped line.* Compose lays an item out as a
marker column beside a text column, so a second line stays
indented; iris writes the marker into the same buffer, so
a wrapped line returns to the left margin. Needs per-line
indent in `TextAttrs`.
3. *A table.* Compose draws a real grid, cells wrapping at a
136dp floor; iris draws padded monospace columns. Same
information, different picture.
4. *Inline code.* Compose draws a chip behind it; iris gives
the range a monospace face and the code colour. Unchanged
by this box -- still blocked on per-range glyph geometry
(IRIS_TODO).
5. *A user message.* Compose draws it in a rounded card;
iris draws a sender label above plain text. That is the
row's own styling, P1's rather than P1a's.
**One real defect found and fixed on the way**, and it is
not a small one: **`Rect::is_size_independent()` answered
`true`.** A `Rect` fills whatever region it is given, so its
content *is* the region -- and `draw_inner`'s
size-independent fast path, which rewrites a widget's
primitives with `r.outside(&from).within(&region)` instead of
redrawing, cannot reproduce that remap once a region carries
both `rel` and `abs`. The visible result: a fenced block's
background kept the height of the **provisional full-region
draw** `Span` does in its first phase, so one fence's panel
covered every block below it *and every row below that*,
while the text underneath was laid out correctly. It answers
`false` now (`iris/src/widget/rect.rs`, with the account at
the definition). This is very likely the same family as this
file's older "the composer bar's grey background is not
drawn" note and any other `.background(rect(..))` tint.
**One defect found and left open**, with its repro:
`UiRenderState::reposition`'s debug assert -- *"widget ... is
both moved by its parent's own layout (`mov`) and
repositioned within it"* -- fires from `List::place` when a
transcript row's blocks **wrap**. Reproduce in one line:
change `.wrap(!verbatim)` to `.wrap(true)` in
`transcript-ui/src/row.rs`'s `build_block` and run
`iris/run-headless.sh transcript --shot /tmp/x.png -- -p
transcript-ui`. It is *not* caused by the `Rect` fix above
(it survives it) and not by any one block kind (bisected: it
appears once the row is tall enough). The shipping
configuration does not reach it -- verbatim blocks do not
wrap -- and neither does the Android bench, which ran clean
with the assertions live. It should be the next thing looked
at under P1, because it is a real inconsistency about who
owns a widget's move slot, not a false alarm.
**Bench, stream phase, this checkout's AVD, debug x86_64
`force-gles`, assertions live, no abort:**
`stream: 294 frames over 21.0s, late 283 (96.3%), p50 53.0ms
p90 108.6ms p99 132.0ms` against the pre-P1a
`p50 52.8ms p90 108.1ms p99 137.3ms` -- unchanged, which is
the point: block styling is span work, not layout work. The
`worst` figure is the one number that moved and it does not
reproduce: 567.3ms, 140.1ms and 664.5ms across three runs of
the same build, against 148.9ms before. Unexplained; it is a
single frame in 294 and the percentiles are flat, so it reads
as an emulator hiccup rather than a cost, but it is written
down rather than rounded off.
**Checks**: `cargo fmt --all --check` clean in both
workspaces; `cargo clippy -p iris -p iris-core -p
transcript-ui -p desktop-app -p tabs-ui --all-targets`
warning-free; `cargo test` 85 (iris, +4) + 13 (iris-core) +
31 (transcript-ui, +11) + 123 (client-core).
- [ ] **P1b — tool-call cards and grouping.** `ToolRows.kt`/
`ToolInput.kt`'s cards: a collapsed row per call with name
and a one-line summary, expand to input and output, runs of