The random tree at depth 7 measured against the two revisions before it: the GPU pass is a tenth of a millisecond everywhere, and every difference is the CPU laying out. A repaint of one leaf costs 1313 draws and 6.5 ms where the pre-#16 code cost one draw, because `redraw` escalates to the top size reader before knowing whether the size changed at all. Drawing first and escalating only on a different size takes that to one draw and takes a resize below the pre-#16 number, but misplaces four widgets in the resize case, so it is the owner's to call. Also records the measure-by-drawing multiplier -- 1.3x the drawn widgets at depth 4, 5.2x at depth 8 -- and that scrolling is now part of the random trees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
480 lines
27 KiB
Markdown
480 lines
27 KiB
Markdown
# 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 `cdec293`, thirteen commits, 50
|
|
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/<id>/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.
|
|
|
|
Three separate costs, in the order they are worth fixing:
|
|
|
|
- **A repaint escalates to the top size reader and redraws its whole
|
|
subtree.** `redraw` asks `mark_readers` first, so a widget whose drawing
|
|
changed but whose *size* did not still redraws everything from the highest
|
|
widget that ever read it. At depth 8 a one-rectangle repaint costs 4825
|
|
draws and 24 ms, which is more than building the tree from nothing. The
|
|
measured alternative is to draw the widget in its own box first, compare
|
|
the `Size` it returns with the one it had, and escalate only when it
|
|
differs: repaint falls to 1 draw and 0.000 ms, a scroll to 11 draws, and a
|
|
resize to 532 draws and 2.3 ms -- below the pre-#16 numbers on every line.
|
|
**It is not correct as written**: the resize case in `tests/generated.rs`
|
|
fails at seed 1 with four widgets misplaced, because drawing the widget
|
|
clears the mark that was the only thing stopping a later parent draw from
|
|
reusing it into a different box. The patch is kept at
|
|
`/tmp/escalate.patch`. This is the owner's call, since it changes when a
|
|
widget is drawn.
|
|
- **A container measures a child by drawing it in a box it will not keep.**
|
|
`Span::draw` places each child without an exact `size_hint` at
|
|
`UiRegion::FULL` to read its length, then places it again at the box it
|
|
worked out, and the second place redraws whatever the first drew. It
|
|
compounds with nesting: draws per drawn widget are 1.3x at depth 4, 3.3x at
|
|
7 and 5.2x at 8. Wrapping text is shaped at the trial width and then again
|
|
at the real one -- 332 shapes for 152 text widgets on a cold frame.
|
|
- **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, 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 <iris-worktree>
|
|
cargo fmt --all --check
|
|
cargo clippy --workspace --all-targets -- -D warnings
|
|
cargo test --workspace
|
|
```
|
|
|
|
49 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 `<ms> down|move|up <x> <y>` 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.
|
|
- **`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<RefCell<..>>`**, 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}`.
|