# Iris extraction handoff Operational handoff for pulling Iris out of ai-app into a standalone framework. Not a decisions log; delete it when the extraction is done. ## Where things stand Canonical `main` is **`ca2b4b2`** (#17, the headless rig). Sixteen slices are in. **#18 `split/18-position-chain`** is open and finished apart from one decision: worktree `/home/bob/repos/iris-pr18`, head `480f0bc`, sixteen commits, workspace tests passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised to boxes. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean. **The one thing waiting on the owner.** `tabs` at 1920x1200 is no longer byte-identical to `upstream/main`: 1,283 pixels of 2.3M (0.06%), two one-pixel-wide panel edges shifted by a pixel, at x=1056 and x=1337. `view`, `minimal` and `text` are identical. Composing a position through the chain in the shader associates the arithmetic differently from collapsing it on the CPU, so a value that used to land exactly on an integer falls the other side of the shader's `floor`. The CPU and the GPU still agree with each other -- both walk the chain bottom-up -- so hit testing matches what is drawn; what changed is only the comparison against the old code. Matching it exactly means composing root-down in the shader, which needs the chain collected into an array first. Byte-identical against `upstream/main` has been the bar for every slice, so this is hers to accept or to spend a commit on. Check for a review before starting anything, and read the newest `submitted_at` rather than the first result: ```sh TOKEN=$(cat ~/.config/gitea/token) N=18 curl -s -H "Authorization: token $TOKEN" \ https://git.arirex.me/api/v1/repos/iris/iris/pulls/$N/reviews curl -s -H "Authorization: token $TOKEN" \ https://git.arirex.me/api/v1/repos/iris/iris/pulls/$N/reviews//comments curl -s -H "Authorization: token $TOKEN" \ https://git.arirex.me/api/v1/repos/iris/iris/issues/$N/comments ``` My replies are ordinary issue comments on the same PR and say what each change was for. ## How a position resolves now Invariants, not history. Everything in `core/src/ui` rests on them. - **A slot holds a box, in the coordinates of the slot it names.** A primitive instance and a mask each name one, and `prelude.wgsl` composes the chain with `within`. A translation is the special case where the box has its parent's relative extent. The identity is `UiRegion::FULL`, **not zero** -- a zeroed entry is a box of no extent and collapses its subtree to a point, which `MoveOffset`'s comment says beside the `Zeroable` that `Pod` requires. - **A slot has to carry a whole box** rather than a scale and an offset: a pixel-space affine map scales everything under it, including a child that must keep its pixel length, and the `rel`/`abs` pair is exactly what distinguishes the two. - **Slots are opt-in.** `Painter::place` draws a child whose box its parent decides and may decide again, and that child gets a slot; `widget` and `widget_within` do not, and share the nearest ancestor's. `Span`, `Aligned` and `Scroll` place. This is what keeps the chain 2-4 deep rather than full tree depth, which is the difference between free and +42.6%. - **A widget's region is held in the coordinates of the slot it draws in**, so a placed widget draws against `UiRegion::FULL` and its box lives in its slot. `ActiveData::region` is the box it was *offered*, in its parent's slot coordinates, and `ActiveData::parent_move` is the slot that is in; `window_region` composes the one through the other, which is the walk the shader does. - **Nothing inverts a lerp.** `UiRegion::stretch`, `stretchable` and `UiScalar::stretch` are gone. A box that changed length is written to its slot and the descendants recompose against it, which also covers the case the old guard refused outright: a fixed length has no fraction to recover, so a 40-tall row could not be stretched on its other axis at all. - **Reuse is decided on the box a widget drew against, in pixels** (`ActiveData::px`). A region is a fraction of a slot's box, so an unchanged region is *not* an unchanged box -- a child drawn at `FULL` of a slot that has since halved compares equal to itself. This is the check everything else rests on; do not weaken it back to comparing regions. - **A size the parent learnt by drawing the child is an answer for that box only.** `redraws_under` redraws a child whose size the widget read unless it declares an exact `size_hint` for the changed axis -- the one case the parent did not have to draw it to find out. The cost is that a size-reading container gives up its reuse when its box changes length, which is every span, so `OnResize::Scale` earns its keep on moves and on subtrees whose sizes nobody read rather than on every stretch. - **`redraws_under` is a question asked before reusing, never a marking.** Marking descendants for redraw instead does not terminate: the mark escalates to that descendant's size reader, which re-places the child, which marks it again. Asking first and giving up the whole reuse adds no marks and stops. - **The walk stops where a length did not change.** A part of a box with no relative extent on an axis is a fixed length held as offsets from that box's start, and composing anything into it leaves no relative extent either -- so a widget whose own box did not change length has no descendant whose box did. An 80-wide child of a widened row is never asked. - **`Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`**; each places in fractions and offsets of its own box and none reads its pixel length. `Scroll` and `MaxSize` read pixels and stay `Redraw`, which is the general rule: `Scale` on an axis unless the draw reads the pixel length of its box on that axis. The default stays `Redraw`. - **`OnResize::Scale` keeps its name.** The owner rejected `Stretch` on 2026-09-14: stretch has an opposite and scale does not, and the answer is per axis, so the axis is already established where it is read. ## Measured, so the next attempt is compared rather than argued | rig | what it says | | --- | --- | | `tests/chain_cost.rs` | GPU pass time by chain depth at 200k instances. Translate slots: free to depth 8 (+5%), then ~3 us per level, +42.6% at 16 and +221% at 64. Each step is a storage load addressed by the previous one, so it is the chaining that costs, not the arithmetic at a level. A box slot (36 bytes) against a translate slot on the same binary: +0.6% at depth 1, +0.5% at 2, +0.8% at 4, then +9.6% at 8 and +32.2% at 64 -- free where opt-in slots put it, and dear only where the chain already was. | | `tests/replace_cost.rs` | Instructions per frame re-placing 200 rows: 1.98M writing each row's slot, 2.38M rewriting its regions, 7.13M redrawing it. A load for `perf`, not a check. Five primitives per row; the regime that decides whether the chain is worth it is a transcript row of a few hundred glyphs, **so re-run it with 200 characters of text per row before concluding anything from it**. | | `tests/draw_cost.rs` | What recording a frame costs on the CPU by layer count. Dispatch per list is 6 instructions, 0.1% of a frame at 256 and at 1024 layers. | A chain is irrelevant at an example's couple of hundred primitives; a transcript's glyphs are tens of thousands, which is the regime `chain_cost` measures. ### How much of that work is necessary Measured 2026-09-14 on a random tree at seed 1, depth 7 -- 1061 widgets, 839 of them drawn, 10,872 primitive instances -- against the same rig at `43ce8c7` (the last commit before #16 sized a widget by drawing it) and at `f942385` (#16 itself). The rig is a `Harness` load counting widget draws, text shapes and instance writes per frame, plus the GPU pass through timestamp queries; it lives in the scratch worktrees `/home/bob/repos/iris-size-{old,new}` and is not in the repository, because the counters it reads are patched into `iris-core`. | per frame | before #16 | #16 | #18 head | | --- | --- | --- | --- | | cold layout | 19.3 ms, 839 draws | 30.9 ms, 3317 | 23.6 ms, 2755 | | repaint one leaf | 0.000 ms, 1 draw | 8.5 ms, 1683 | 6.5 ms, 1313 | | scroll one scroller | 0.001 ms, 1 draw | 8.9 ms, 1683 | 6.5 ms, 1313 | | resize the output | 3.2 ms, 839 draws | 31.8 ms, 6940 | 25.2 ms, 5512 | | GPU pass | 0.129 ms | 0.114 ms | 0.115 ms | **The GPU is not the subject.** The pass is a tenth of a millisecond at every revision and every phase; all of this is the CPU laying out. `a640c6c` and `84f589e` remove most of the CPU work without weakening the retained-layout rules. Against the same seed-1/depth-7 load, widget draws are now 2,117 cold, 1 for a leaf repaint, 11 for a scroll, and 3,139 for a resize (from 2,755, 1,313, 1,313, and 5,512 respectively). Workspace tests pass; the five reference renders, resize render, and image-tab replay are byte-identical to the prior #18 head. Those draw counts explain mechanism, not total layout cost. Before #16, measurement was a separate operation: the cold and resize rows each made 839 draws plus 489 size queries, 314 of which hit the size cache. Current sizing is a draw, so its draw count includes the provisional work and cannot be compared directly with the old draw count. Fresh release wall-time measurements put the complete CPU layout cost in perspective: | per frame | before #16 | `84f589e` | | --- | --- | --- | | cold layout | 18.9 ms | 20.5–22.0 ms | | repaint one leaf | below timer resolution | below timer resolution | | scroll one scroller | below timer resolution | 0.002 ms | | resize the output | 3.14 ms median | 12.3–13.4 ms median | Wall time moves with CPU frequency, so retained benchmark counters should report CPU cycles or instructions as the deciding total, then separate widget draws, size queries/probes, text shapes and primitive writes to explain it. A whole-rig `perf stat` run, dominated by 100 resize frames, measured 13.76B instructions current versus 3.56B before #16 (3.87x), consistent with the resize wall-time gap. A follow-up rig should select one phase per invocation so the hardware counters are phase-specific rather than inferred from that total. `480f0bc` retains that instrumentation behind the `layout-diagnostics` Cargo feature; none of it is compiled into a normal Iris build. The ignored `tests/layout_diagnostics.rs` rig selects `cold`, `repaint`, `size`, `scroll`, or `resize` with `IRIS_PHASE`, plus seed, depth and frame count. Run it with the feature for explanatory counters and inclusive phase timers, and without the feature under `perf` for unperturbed CPU totals: ```sh IRIS_PHASE=resize IRIS_DEPTH=8 IRIS_FRAMES=100 \ cargo test --release --features layout-diagnostics \ --test layout_diagnostics -- --ignored --nocapture IRIS_PHASE=resize IRIS_DEPTH=8 IRIS_FRAMES=1000 \ perf stat -e cycles:u,instructions:u cargo test --release \ --test layout_diagnostics -- --ignored --nocapture ``` The first depth-8 resize run makes the amplification concrete. A 260-widget tree has 215 active widgets, but a resize averages 1,395 widget draws, 913 placement calls, 1,313 reads of drawn sizes and 34,844 primitive writes. Only 12 distinct text widgets render, yet they render and reshape 282 times per frame with no shape-cache hits. Text rendering accounts for 11.6 of 13.2 ms, including 9.9 ms shaping and 1.7 ms placing glyphs. The hottest two text widgets each draw 96 times per frame at 22 distinct widths across two resize frames, below nested `Span`, `Aligned`, `Scroll`, `Pad`, and `SetSize` ancestors. This identifies repeated constraint discovery, rather than resize marking (0.001 ms), as the next subject; it does not yet choose between per-axis retained size validity and coalescing the resize dependency frontier. The generated tree now includes `Aligned` with every meaningful per-axis alignment. That exposed two retained-layout ordering bugs which `84f589e` fixes. The regular cold-layout equivalence suite passes. The ignored 100-seed sweep passed every transition through seed 59 and now gets past the former scroll failure at seed 52; at seed 60 it reaches the already-documented iterative wrapping-text defect, differing by 0.00003 px after `SwapForThree`. The branches are invariants rather than widget exceptions: - A dirty widget draws locally first only while its retained box has the same pixel size. If its returned `Size` is unchanged, no size reader can observe the repaint and no ancestor draws. If the size changed, the dependent path lays out. A changed pixel box takes the conservative path first, which is the condition the discarded `/tmp/escalate.patch` missed on resize. - Dirty widgets settle deepest-first. A changed size queues only its immediate reader; propagation stops as soon as a reader's own answer stays unchanged. During an output resize, the resize condition stays live until these updates finish, so an `Aligned` ancestor chooses the new child box before a pixel-dependent descendant draws in it. - A span measures an unknown child in the part of its axis still available, rather than giving every child the whole container and immediately taking most of it away. This is the archive's faster shape, recreated on the current types; it often makes the measurement box the final box without caching an answer under different constraints. A proposed `Scroll` shortcut that reused its direct child's retained size was discarded. “Direct child is clean” is not strong enough while a dirty descendant's structural change is still propagating; seed 52 demonstrated the stale-size failure. Re-measuring the scroll subtree costs 11 draws rather than 1, but remains two orders of magnitude below the old 1,313 and follows the actual dependency invariant. Do not restore the archive's old `DrawMode::Measure`. It skipped primitive and retained-state writes while still walking widgets and shaping text, and once improved a streamed-frame benchmark from 1.39 to 1.22 ms p50. Retained placement later replaced it because a provisional draw is usually already usable in its final box; measure-only mode would discard that useful output and force another real traversal. The owner confirmed on 2026-09-14 that direct placement is the intended path. The remaining costs are: - **A container measures a child by drawing it in a box it will not keep.** The remaining-region trial removes many mismatches, but an unknown child's measured length can still make its final box differ, and nested containers compound those redraws. This is now the largest CPU layout cost. - **Redundant draws rewrite primitives.** A cold frame writes 48,050 instances for a tree that holds 10,872. What reaches the GPU is 10,872, since a layer uploads whole, so this is CPU cost only -- but a one-leaf repaint still uploads 5,863 instances where the pre-#16 code uploaded 106. ## The random trees `iris::random` grows a seeded tree -- spans in every direction holding two to four children, stacks, scrolls on either axis, alignment on either axis, padding with each of its four sides its own number, rects with varying opacity, text both wrapping and overflowing, a declared size over half of it, stopping at a depth. `examples/random.rs` draws one (`IRIS_SEED`, `IRIS_DEPTH`). `tests/generated.rs` grows each seed twice -- once and then changed, once with the change built in -- and compares every widget's box across eight scenarios: a size change, a resize, both, and five ways of changing what a span holds. `a_long_run_of_seeds_agrees` is the ignored sweep, 100 seeds across all eight, 800 comparisons, about four minutes. The property is that laying a tree out again lands where growing it cold does, which is the same thing as layout being a function of the state. It earned itself immediately: it found the non-terminating marking, the pixel-box reuse check and the measured-size rule above, none of which the hand-written tests reached, and it says the result is better than what it started from -- 90 of 90 against 83 on `db1751f`. Four things about it that are easy to get wrong: - **Both trees must make the same widgets in the same order.** Comparison is index for index, so a tree that makes fewer widgets, or frees one whose id is then handed to the next, stops lining up at the first difference and every comparison after it is against the wrong widget. Hence three spare leaves grown beside every span whether they end up in it or not, and detached children held until the comparison is over. - **Attaching a spare moves it.** A widget belongs to one parent; `WeakWidget::upgrade` registers an add and panics with "cannot add a widget twice", so it is for a handle that was never added, not a second share. - **Each shuffle asserts the tree actually changed** before comparing, or a case that quietly did nothing passes green. - **A failure prints the widget's ancestry**, marking the ones that own a slot, because where two trees disagree is rarely where the cause is. ## Verifying a slice ```sh cd cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` 50 tests pass on #18's head. `--workspace` matters: `rig-input` is a crate of its own. Render checks are the last pass, not the iteration loop -- the owner asked for that on 2026-09-14, since the layout tests cover the CPU part and the shots cost real time: ```sh ./scripts/run-headless.sh tabs --mode 1920x1200@60Hz --shot /tmp/out.png ./scripts/run-headless.sh tabs --replay /tmp/taps.touch --shot /tmp/out.png ./scripts/run-headless.sh tabs --mode 1920x1200@60Hz --resize 900x1200@60Hz --shot /tmp/rs.png ``` - The reference shots are `tabs`, `view`, `minimal` and `text` at 1920x1200, plus `tabs` with a replay that switches to the image tab and adds two images. A `.touch` line is ` down|move|up ` in the output's own pixels; the tab strip is at y=24 and the five tabs at x = 192, 576, 960, 1344 and 1728, with the image tab's add button near (1836, 1116). - **A resize is its own case**, and `--resize` is it: the output changes under the running app, and what it lands on must match a cold start at that size byte for byte. That caught both of #16's defects and nothing in `cargo test` can see it. - **Run one at a time.** The rig reuses a single compositor and a single output, so two invocations at once resize each other's window and quietly screenshot the wrong thing. Two sets of shots were thrown away learning that. - **Give a comparison worktree its own target dir.** While one was shared between two checkouts I got results I could not reproduce afterwards; the mechanism was never pinned down, so re-run any cross-checkout comparison in isolation before believing it. Two drawing paths still have no shot of their own, and each needs a ui the examples do not have, so both are throwaway examples written into the worktree and deleted after: 1. An image alone in a layer, which is the case that failed GPU validation when every other test happened to have a rectangle in the same layer. 2. Six lines of 400px text, which forces the atlas to four pages and proves the array grew and its group was rebuilt. `tabs` with the image replay covers rects, glyphs and images together, so that one is an ordinary check now. ## How the work is sequenced **Most fundamental first**, from the owner on 2026-09-13: *"please do more fundamental changes first, such as library updates and core framework changes, so that code only has to be written once"*, and *"should probably start adding tests early on rather than later, so you don't have to make separate test scripts and stuff."* So slices are ordered by how much depends on them, not by what is nearest ready, and a slice arrives with tests rather than with a script in `/tmp`. **Agree a design before sending another variation of it.** The owner stopped the fourth round of #11 with *"we should probably agree on the design here rather than you keep submitting variations that I review"*. When a review comes back about the shape of something rather than a defect in it, put the options and a recommendation in front of her and implement what she picks. **Nothing is submitted without a separate review pass** -- the installed `pre-submit-review` skill: build clean, review the code, review the comments on their own once the code has settled, then verify the claim by running it. The fixes a review produces are themselves unreviewed code, so the passes repeat until a round finds nothing. It has earned its place repeatedly: four defects on #11 that format, clippy, tests and five headless renders had all passed, and on #12 a regression introduced by the review's own first draft. `audit.sh` in the skill directory prints every comment line a branch adds against a base ref; the owner's standing complaint is verbose agent comments, and the default verdict is delete. **Machine-specific notes do not belong in the repository** -- they live in `~/.claude/MACHINE.md` or a `this-machine-*` skill. Other standing instructions from the owner: - Pull Iris out even if the Rust application switchover is not accepted. No app, session, transcript, setup or server concepts in Iris; the dependency runs one way from `app/` to Iris. - **Small, coherent PRs.** The original extraction PR was too large to review. A slice may be redone rather than transplanted, and need not remove every old feature. Non-conflicting pull requests may be open at once -- disjoint path sets, each branched from current `upstream/main` rather than stacked. She reviews small ones as they arrive and only avoids two *large* ones in flight. - **Order by dependency, largest reach first.** On 2026-09-13: *"do the large reaching framework changes first so less has to be redone."* - Do not recreate an `ai` branch in canonical Iris; the fork is the boundary. - **Never rewrite a pushed branch.** Follow review with additive commits, and merge `upstream/main` in rather than rebasing when a branch falls behind. - Respond to each review finding with a fix or a concise explanation. Do not add a ceremonial comment when the changed code already answers it. - **A test has to guard something that could break again.** She deleted #14's test as pointless: the rename it guarded cannot regress. When a fix is structural, the structure is the test. - **Say who decided a constraint.** LAYOUT.md §2's "move slots carry translation only" was written by an agent on 2026-09-04, was never asked for, and read as settled until she said *"I was not aware that an agent decided position slots should be translate only."* Mark an agent's own choice as one. ## What is left **Next, and small:** `LazySpan`, the last of LAYOUT.md §2. `set_child_offset` is no longer part of it -- a child offset is just placing the child, which `Painter::place` now does. Then, roughly in dependency order: - **Built-in alignment, and probably size**, which the owner moved ahead of the rest on 2026-09-14. Reproduced in the harness: `.width(rel(0.5))` inside a `Dir::DOWN` span reports 200 of 400 and is handed the whole 400, and a `Pad` in between does not change that. **Do not "fix" it by reading the child's ortho `size_hint`** -- a `Pad` between the `SetSize` and the span has no hint of its own, so the declared width silently goes back to filling. It works only when nothing is in the way. Alignment has to belong to the widget rather than be discovered through whatever happens to sit on top of it. Two things beyond the bug argue for it. Built-in size removes `SetSize`, and with it a wrapper reporting one size while handing its child the whole box. And built-in alignment is what would let `OnResize::Translate` apply to centred content, which otherwise has to say `Redraw` because only its own draw knows where the middle was. Size is the harder half: a declared size beside the one `draw` returns is two sources of truth for one thing, so settle what each means before building it. This is independent of #18's retained-update fix now that `Aligned` is in its generated coverage; land #18 first, then design built-in alignment and size together as the next structural slice rather than mixing that representation change into this performance correction. - **`OnResize::Translate`, which still does nothing.** The chain removed half its obstacle: a placed widget's slot holds the box it was offered while its drawing is a set of fractions of that box, so the two are no longer one field. What is still missing is a widget saying *where* in a bigger box its unchanged drawing should sit, which is the alignment work above. - **`UiRenderState` behind `Rc>`**, queued by the owner on 2026-09-13 as fundamental, and especially so for text. - **`Len`, `LayoutLen` and dp.** The archive splits the type so that `rest` is unrepresentable where it is meaningless (a padding), and folds a density in at resolve time. 21 files mention `Len`, so it is wide but shallow. - **The input restructure** -- `src/default/sense.rs` becomes `src/rsc/sense.rs` (308 lines to 2313), plus `core/src/event/controller.rs`, `desktop/input.rs`, `android/input.rs` and `sense_tests.rs`: pointer capture, drag slop and axis, platform cancellation, mask-aware hit testing, event timestamps. The archive's own `consumes` is what #12 landed, so that part transplants; `tests/pointer_routing.rs` is the acceptance criterion. - Retained span, scrolling and layout placement. - Retained paints, selection, overlays and shared UI runtime state. - Generic desktop/Android framework hosts and reusable example/APK tooling. - Application-owned fonts and application-named font families. - Shared resource-handle bookkeeping and replaceable glyph-atlas buckets. - Positioned text overflow and cluster-safe ellipsis. Dependencies are current apart from `winit`, which stays on 0.30.12 until 0.31 leaves prerelease. `parley` 0.11.1 and `image` 0.25.10 are latest. The archive is a reference, not a patch to apply -- it writes `Widget::draw` against `painter.set_size`, which #16 replaced with a returned `Size`, and lengths against `LayoutLen` and `density`, which canonical does not have. Recreate a change on today's types, leave app-specific behaviour out, and verify it independently. ```sh cd /home/bob/repos/iris && git fetch upstream git diff --stat upstream/main..origin/archive/full-extraction ``` ## How the renderer works now Current invariants, not history. Worth reading before touching `core/render`. - **A primitive registers itself by being drawn.** The type carries its own WGSL, and `PrimitiveRegistry` keys ids by `TypeId`, so the kind comes from the type and there are no `RECT`/`GLYPH`/`TEXTURE` constants. Nothing is seeded, so an id depends on what a ui drew first and a ui pays only for the pipelines it uses. - **Each primitive records its own draws.** `Primitive::render` makes a `PrimitiveRender` that states the layout its shader reads, uploads whatever it owns, and records its draws. `GlyphRender` owns the atlas and binds it once per list; `ImageRender` owns the images and binds one per instance; the default owns nothing and draws every instance in one call. The renderer sets the pipeline, the shared group, the list's data and its vertex buffer, and knows nothing else. - **The shared bind group is the window, the masks and the move chain**, given to every draw. A mask texture would go here too. What a primitive samples is its own group, and a primitive that samples nothing has no such group in its pipeline. - **Every binding size is stated.** A `None` minimum puts the binding on wgpu-core's late-sized list, which `is_ready` scans on every draw. - **`shader/prelude.wgsl` plus one file per primitive**, because one module cannot declare two types at the same binding. The prelude carries only what every primitive uses -- window, masks, the chain walk, the vertex shader, `masked()` -- and its header is where binding numbers are written down. - **A texture handle is drawn like anything else.** `Painter::primitive` takes `impl PrimitiveLike`: a primitive, or something that yields one and does whatever else drawing it needs -- a `&TextureHandle` retains its share on the way through, which a `Pod` primitive cannot. - **Order within a layer means nothing**, and the widgets do not rely on it: `Stack` gives each child its own layer and `TextEdit` draws its view in a child layer above the selection rectangles. - **Images are one texture and one bind group each**, so each drawn image is a draw call. The owner chose that on 2026-09-13 over packing images into arrays like atlas pages; a bindless `binding_array` was ruled out by Android support. Revisit only with her. - **Layers are never freed** (`TODO` in `primitive/layer.rs`), so every layer a session creates is walked every frame thereafter. Measured at ~2ns per empty layer per frame, which is why it is the TODO's problem and not a bug of its own. ## Repository topology ### ai-app checkout - `/home/bob/repos/ai-app-2`, `origin = git@git.arirex.me:iris/ai-app.git`, branch `rustify`. - `iris/` is a submodule pinned at `32f6ad8`, the complete extracted snapshot, and `.gitmodules` points at the **bot fork**, not canonical Iris. - Do not change either casually: ai-app needs the complete snapshot while canonical Iris is only partly caught up. Reconcile when canonical contains what ai-app needs, or when the owner accepts a temporarily non-building pin. ### standalone Iris checkout - `/home/bob/repos/iris`, `origin` = fork, `upstream` = canonical. - Fork `main` and `origin/archive/full-extraction` both name `32f6ad8`, the target snapshot. `history/full` names the source-history result `a615bcd`. - **Do not reset, overwrite or force-push fork `main`**: it is both the target reference and the commit ai-app pins. - Start each new branch from current `upstream/main` in its own worktree: ```sh cd /home/bob/repos/iris && git fetch upstream git worktree add -b split/19-name /home/bob/repos/iris-pr19 upstream/main ``` `/home/bob/repos/iris-pr18` is the live one. Every other `iris-pr*` worktree holds a merged branch; they are readable references, not places to build. ## Cautions - Read `/home/bob/repos/ai-app-2/AGENTS.md` and the machine-wide rules first. Anything about this machine -- the GPU that comes and goes, measuring a small performance difference, the emulator -- is in `~/.claude/MACHINE.md` and the `this-machine-*` skills, and belongs there rather than here. - Keep Iris generic: session drivers, transcripts, setup and server concepts, app icons and product fonts stay in ai-app. Android and desktop code is Iris work only when it is a generic host or platform integration. - Preserve the dirty-worktree rule. All worktrees were clean at handoff; anything found later may be the owner's or another agent's. - Do not delete the archived snapshot or the fork `main` ai-app pins. - A complete target branch is not permission to recreate the giant PR. - Another agent was freeing disk on this VM and removed `target/` from the `iris-pr*` worktrees once. Sources and git state were untouched. Tell peers before changing shared machine tooling, and expect a cold rebuild sometimes. ## Merged so far | PR | On canonical `main` | | --- | --- | | #2 | Build on the current nightly (`4275314`) | | #3 | Request a frame after resize (`936fbdd`) | | #4 | Decouple `iris-core` from winit (`465e430`) | | #5 | Use vsync by default (`ec2b5d4`) | | #6 | Notify winit before presenting (`db9b0f2`) | | #7 | Keep unsafe reference helpers internal (`0191f20`) | | #8 | Initialize the window uniform from the surface (`6e271e8`) | | #9 | Preserve primitive-count recursion (`b90c855`) | | #10 | Text layout and rendering on Parley (`0f6a28b`) | | #11 | Atlas as an array texture, and the primitive rendering overhaul (`b234497`) | | #13 | Build on wgpu 30 (`00d2230`) | | #14 | Rename the `Sized` widget to `SetSize` (`32b1038`) | | #15 | Run a ui without a window, and test one (`c8ac669`) | | #12 | Route pointer input per kind (`43ce8c7`) | | #16 | Size a widget while drawing it, not in a pass of its own (`f942385`) | | #17 | Bring the headless rig into the repository (`ca2b4b2`) | URLs are `https://git.arirex.me/iris/iris/pulls/{number}`.