Rewrite the extraction handoff as current state

Incremental edits had spliced the opening section into two half-sentences and
left it saying main was at the commit before last. It is now written as where
things are: two open PRs, the harness as the next slice, the renderer's
invariants rather than the story of how they arrived, and the three render
checks described rather than pointed at a script in /tmp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-13 19:45:08 -04:00
1 parent c2b1c07895
commit d0606f46ec
1 file changed
+190 -199
+190 -199
View File
@@ -3,106 +3,70 @@
Operational handoff for pulling Iris out of ai-app into a standalone framework. Operational handoff for pulling Iris out of ai-app into a standalone framework.
Not a decisions log; delete it when the extraction is done. Not a decisions log; delete it when the extraction is done.
## Read this first ## Where things stand
**#11 is merged** (`b234497` on canonical `main`, 2026-09-13). Open now are Canonical `main` is **`00d2230`** (#13, wgpu 30). Eleven slices are in; two
#12 (pointer routing, never reviewed) and #13 (wgpu 30). Check for a review pull requests are open and both are waiting on the owner, not on me:
on either before starting anything new:
- **#12** `split/12-pointer-routing`, worktree `/home/bob/repos/iris-pr12`,
head `71ba372`. A widget takes only the input it answers to, and only where
the cursor is. Self-reviewed under the process below, which found two
defects: consumption decided once a frame rather than per input kind, and a
press firing on a widget the cursor had just left. Six tests across layers;
three of them fail on `main`.
- **#14** `split/14-setsize`, worktree `/home/bob/repos/iris-pr14`, head
`f312db6`. The `Sized` widget becomes `SetSize`, so the prelude stops
shadowing the marker trait and `?Sized` resolves again in any crate that
imports it. `tests/prelude_bounds.rs` fails to compile if that comes back.
Check for a review before starting anything, and read the newest
`submitted_at` rather than the first result:
```sh ```sh
TOKEN=$(cat ~/.config/gitea/token) TOKEN=$(cat ~/.config/gitea/token)
N=12
curl -s -H "Authorization: token $TOKEN" \ curl -s -H "Authorization: token $TOKEN" \
https://git.arirex.me/api/v1/repos/iris/iris/pulls/13/reviews https://git.arirex.me/api/v1/repos/iris/iris/pulls/$N/reviews
``` Check for one before doing anything
else:
```sh
TOKEN=$(cat ~/.config/gitea/token)
curl -s -H "Authorization: token $TOKEN" \ curl -s -H "Authorization: token $TOKEN" \
https://git.arirex.me/api/v1/repos/iris/iris/pulls/11/reviews https://git.arirex.me/api/v1/repos/iris/iris/pulls/$N/reviews/<id>/comments
curl -s -H "Authorization: token $TOKEN" \ curl -s -H "Authorization: token $TOKEN" \
https://git.arirex.me/api/v1/repos/iris/iris/pulls/11/reviews/<id>/comments https://git.arirex.me/api/v1/repos/iris/iris/issues/$N/comments
curl -s -H "Authorization: token $TOKEN" \
https://git.arirex.me/api/v1/repos/iris/iris/issues/11/comments
``` ```
Check the newest `submitted_at` rather than assuming the first result is My replies are ordinary issue comments on the same PR and say what each change
current. My replies are ordinary issue comments on the same PR and say what was for. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean.
each change was for.
## 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. This might involve making the harness eventually, depends
on what needs tested."*
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 **Agree a design before sending another variation of it.** The owner stopped
the fourth round with *"we should probably agree on the design here rather the fourth round of #11 with *"we should probably agree on the design here
than you keep submitting variations that I review"*. When a review comes back rather than you keep submitting variations that I review"*. When a review comes
about the shape of something rather than a defect in it, put the options and a back about the shape of something rather than a defect in it, put the options
recommendation in front of her and implement what she picks. She chose the and a recommendation in front of her and implement what she picks.
per-image bind group for textures on 2026-09-13; the alternatives were
packing images into arrays like atlas pages, and a bindless `binding_array`
(ruled out by Android support).
## Stop point **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.
- Canonical Iris: `git@git.arirex.me:iris/iris.git`, `main` at `0f6a28b` Other standing instructions from the owner:
(the squash merge of #10).
- Agent fork: `git@git.arirex.me:iris-ai/iris.git`. The bot cannot push
canonical; work in the fork and open PRs into `iris/iris:main`.
- Canonical `main` is `00d2230` (#13, wgpu 30). `/home/bob/repos/iris-pr13`
is a merged branch now; do not build new work on it.
- **#14** `split/14-setsize`, worktree `/home/bob/repos/iris-pr14`, head
`f312db6`, one commit on `00d2230`. The `Sized` widget becomes `SetSize`,
which stops the prelude shadowing the marker trait. Not reviewed yet.
- **#12** `split/12-pointer-routing`, worktree `/home/bob/repos/iris-pr12`,
head `71ba372`, current `main` merged in. A widget takes only the input it
answers to, and only where the cursor is. Self-reviewed under the new
process, which found two defects: consumption decided once a frame rather
than per kind, and a press firing on a widget the cursor had just left.
Six tests across layers; three fail on `main`. Waiting on the owner.
- `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean.
## What #11 became
It started as "draw the glyph atlas as an array texture" and four rounds of
review turned it into a rework of how primitives are drawn. Worth knowing
before reading a new review, because the earlier rounds are what produced this
shape:
- **Atlas pages are their own thing.** `GlyphAtlas` owns its page images and
emits dirty rectangles; `GpuPages` owns the array texture they upload to.
`Textures` holds standalone images only.
- **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 what it uses.
- **Nothing dispatches dynamically.** Push, free, renumber, upload and draw are
identical for every primitive; only the entry size and the pipeline differ,
and those are data. `InstanceList` carries a runtime stride and its data as
bytes. Measured: 0.2ns per write over a statically typed list, against 1.6ns
for a trait object.
- **The shared group is what every pipeline gets**: the window and the masks.
A mask texture would go here too.
- **Each primitive records its own draws.** `Primitive::render` makes a
`PrimitiveRender` that states the layout its shader reads, uploads what 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. The renderer knows nothing about textures.
**Measured**, because dispatch per list was the objection: 6 instructions
per list, 0.1% of a frame at 256 and at 1024 layers, against the ~5,400
wgpu spends recording one. `tests/draw_cost.rs` is that measurement.
- **`shader.wgsl` became `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, vertex shader,
`masked()` -- and its header is where binding numbers are written down; what
a shader samples is declared by that shader.
- **A texture handle is drawn like anything else.** `Painter::primitive` takes
`impl PrimitiveLike`: a primitive, or something that yields one and does what
else drawing it needs -- a `&TextureHandle` retains its share on the way
through. The blanket impl and the one for `&TextureHandle` do not collide,
because `Primitive` is ours and no crate can add the overlapping impl.
- **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. Verified with a ui that
registers glyph before rect.
## Standing instructions from the owner
- Pull Iris out even if the Rust application switchover is not accepted. No - Pull Iris out even if the Rust application switchover is not accepted. No
app, session, transcript, setup or server concepts in Iris; the dependency app, session, transcript, setup or server concepts in Iris; the dependency
@@ -111,28 +75,94 @@ shape:
A slice may be redone rather than transplanted, and need not remove every old A slice may be redone rather than transplanted, and need not remove every old
feature. Independent PRs may be open at once so work continues during review. feature. Independent PRs may be open at once so work continues during review.
- Do not recreate an `ai` branch in canonical Iris; the fork is the boundary. - Do not recreate an `ai` branch in canonical Iris; the fork is the boundary.
- **Never rewrite a pushed branch.** Follow review with additive commits. - **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 - 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. add a ceremonial comment when the changed code already answers it.
## The review gate ## The next slice
`~/.claude/MACHINE.md` now carries a machine-wide rule, and the procedure is **The headless rig and the test harness.** `scripts/run-headless.sh`,
the installed **`pre-submit-review`** skill. It is not optional and not `headless.conf`, `rig-input` and `iris::harness` live in ai-app's iris
project-specific: build clean, review the code, review the comments separately submodule and not in canonical Iris. Until they are extracted, Iris can only
once the code has settled, then verify the claim by running it. The fixes a be tested by what runs without a window, and every rendering claim is verified
review produces are themselves unreviewed code, so the passes repeat until a by hand from another checkout. Extracting them is what lets later slices
round finds nothing. arrive with tests instead of screenshots. Nothing else is claimed.
Running it on #11 found four defects that format, clippy, tests and five Still in the target, roughly in dependency order:
headless renders had all passed — including one hidden *by* the tests passing,
where an image alone in a layer failed GPU validation because every other case
happened to have a rectangle in the same layer leaving a bind group set. That
is the shape to hunt for.
`audit.sh` in the skill directory prints every comment line a branch adds - **The input restructure** — `src/default/sense.rs` becomes `src/rsc/sense.rs`
against a base ref, for an explicit verdict each. The owner's standing (308 lines to 2313), plus `core/src/event/controller.rs`, `desktop/input.rs`,
complaint is verbose agent comments; the default verdict is delete. `android/input.rs` and `sense_tests.rs`: pointer capture, drag slop and axis,
platform cancellation, mask-aware hit testing, event timestamps. It replaces
the file #12 fixes, and **the archived copy carries the same per-frame
consumption defect #12 removes** — extracting it as-is brings the bug back,
so `tests/pointer_routing.rs` is the acceptance criterion for that slice.
- Widget draw size and measurement cleanup (source commit `6671194`).
- 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.
```sh
cd /home/bob/repos/iris && git fetch upstream
git diff --stat upstream/main..origin/archive/full-extraction
```
The archive is a reference, not a patch to apply. Recreate a change on top of
canonical `main`, leave app-specific behaviour out, and verify it
independently. Pick disjoint path sets when two PRs are open, and branch each
from the latest `upstream/main` rather than stacking — unless the slice fixes
code another open branch replaces, in which case say so and stack deliberately.
## 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. Dispatch per list was measured at 6 instructions, 0.1% of
a frame at 256 and at 1024 layers, against the ~5,400 wgpu spends recording
one list; `tests/draw_cost.rs` is that measurement.
- **The shared bind group is the window and the masks**, 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, vertex shader, `masked()` — and its
header is where binding numbers are written down; what a shader samples is
declared by that shader.
- **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 ## Repository topology
@@ -157,11 +187,65 @@ complaint is verbose agent comments; the default verdict is delete.
```sh ```sh
cd /home/bob/repos/iris && git fetch upstream cd /home/bob/repos/iris && git fetch upstream
git worktree add -b split/13-name /home/bob/repos/iris-pr13 upstream/main git worktree add -b split/15-name /home/bob/repos/iris-pr15 upstream/main
``` ```
The other `/home/bob/repos/iris-pr*` worktrees hold merged branches. They are Every other `/home/bob/repos/iris-pr*` worktree holds a merged branch. They
readable references; do not build new work on them. are readable references; do not build new work on them.
## Verifying a slice
```sh
cd <iris-worktree>
cargo fmt --all --check
cargo clippy --all-targets -- -D warnings
cargo test
```
`--workspace` when the slice crosses workspace crates. A rendering claim needs
a real run, which until the rig is extracted means driving it from ai-app's
submodule:
```sh
cd /home/bob/repos/ai-app-2/iris
./scripts/run-headless.sh tabs --dir /home/bob/repos/iris-prNN \
--shot /tmp/out.png --seconds 3
./scripts/run-headless.sh tabs --dir ... --replay /tmp/taps.touch --shot ...
```
A `.touch` file is `<ms> down|move|up <x> <y>` in the output's own pixels. The
`tabs` example's five tabs sit at x = 192, 576, 960, 1344 and 1728 on a
1920x1200 output; tapping the third and then the bottom-right button twice
adds two images.
Three renders cover the drawing paths, and each needs a different ui, so two
of them are throwaway examples written into the worktree and deleted after:
1. `tabs` with that replay — rects, glyphs and images together.
2. 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.
3. Six lines of 400px text, which forces the atlas to four pages and proves
the array grew and its group was rebuilt.
Those three become ordinary tests with the harness slice, which is the
argument for doing it next.
## 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 ## Merged so far
@@ -180,96 +264,3 @@ readable references; do not build new work on them.
| #13 | Build on wgpu 30 (`00d2230`) | | #13 | Build on wgpu 30 (`00d2230`) |
URLs are `https://git.arirex.me/iris/iris/pulls/{number}`. URLs are `https://git.arirex.me/iris/iris/pulls/{number}`.
## Choosing the next slice
**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. This might involve making the harness eventually, depends
on what needs tested."*
So the order is by how much else depends on a thing, not by what is nearest
ready, and a slice lands with tests rather than with a script in `/tmp`.
1. **wgpu 28.0.0 -> 30.0.1.** Done, open as #13.
`winit` stays on 0.30.12 until 0.31 is out of prerelease; `parley` 0.11.1
is current. The API changes were: the instance takes the display handle it
presents on (GLES on Wayland needs it), `get_current_texture` returns a
status enum rather than a `Result`, presenting moved to the queue, and
bind group and vertex buffer layouts became sparse.
2. **Next: the headless rig and the test harness** -- `scripts/run-headless.sh`,
`headless.conf`, `rig-input` and `iris::harness` live in ai-app's iris
submodule, not in canonical Iris. Until they are extracted, canonical Iris
can only be tested by the four `tests/text_edit.rs` tests, and anything
about rendering is verified by hand from another checkout. Extracting them
is what lets every later slice arrive with tests.
3. Everything below, once those two are in.
Still in the target and not yet claimed:
- **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. It replaces
the file #12 fixes, and **the archived copy carries the same per-frame
consumption defect #12 removes** -- extracting it as-is brings the bug back,
so `tests/pointer_routing.rs` is the acceptance criterion for that slice.
- Widget draw size and measurement cleanup (source commit `6671194`).
- 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.
```sh
cd /home/bob/repos/iris && git fetch upstream
git diff --stat upstream/main..archive/full-extraction
```
The archive is a reference, not a patch to apply. Recreate a change on top of
canonical `main`, leave app-specific behaviour out, and verify it
independently. Pick disjoint path sets when two PRs are open, and branch each
from the latest `upstream/main` rather than stacking.
## Verifying a framework slice
```sh
cd <iris-worktree>
cargo fmt --all --check
cargo clippy --all-targets -- -D warnings
cargo test
```
`--workspace` when the slice crosses workspace crates. A rendering claim needs
a real run, which on this machine means the headless rig in ai-app's submodule:
```sh
cd /home/bob/repos/ai-app-2/iris
./scripts/run-headless.sh tabs --dir /home/bob/repos/iris-prNN \
--shot /tmp/out.png --seconds 3
./scripts/run-headless.sh tabs --dir ... --replay /tmp/taps.touch --shot ...
```
A `.touch` file is `<ms> down|move|up <x> <y>` in the output's own pixels; the
script replays it through a virtual pointer. The `tabs` example's five tabs sit
at x = 192, 576, 960, 1344 and 1728 on a 1920x1200 output. Read the
`this-machine-graphics` skill before trusting any of it — the Vulkan device
here comes and goes, and everything silently falls back to software when it
does.
## Cautions
- Read `/home/bob/repos/ai-app-2/AGENTS.md` and the machine-wide rules first.
- 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.