b53a17c4364364b37e3cf808f52d94df898645b4
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
25370731d0 | Prune commentary and stale Rust port notes | ||
|
|
3c7d3db370 |
iris: the arenas upload deltas, and stop being 11x bigger than the tree
Changing any primitive re-uploaded every primitive. Measured over the bench fixture by the new arena_churn rig: 758 MB across a fling and 1.2 GB across 401 streamed deltas, p50 3.0 MB per streamed frame. Three separate things were wrong, and only the first is what it looked like from the outside. ArrBuf reallocated on every length change. A fresh Buffer's contents are undefined, so adding one glyph -- which a streamed reply does constantly -- forced a full rewrite, and no partial upload could have been correct in the first place. It has a capacity now, growing geometrically and never shrinking, and update() answers whether the Buffer identity moved so a caller can rebuild its bind group and force the whole range dirty. That alone took the glyph array from 95% re-uploaded to 3%, and stopped primitive_group being rebuilt on every frame the arena changed. A redraw freed its primitives and pushed new ones. Freed slots are not reusable until the end of the frame -- a layer's draw order still names them -- and Painter::draw_twice is how a container learns a child's size, so with containers nested the arena's high-water was the transient push count rather than the live one: 17 million pushes across 401 deltas, 127,443 slots for 11,569 live primitives, growing linearly with the transcript. A redraw now gets its old handles back as a recycle pool (Painter::take_recycled, Primitives::recycle) and writes into the slots it already holds; the pool is consumed in order and whatever the draw does not claim is freed when it ends. The arena is exactly the live count now. The CPU frame improved with it, from p50 2.20ms to 1.39ms on the stream run, because the freeing and the draw-order renumbering went away. Nothing tracked which entries changed. util::Dirty is a bitset per uploaded array, coalesced into ranges at a 1 KiB gap. Marking is O(1) and allocation-free; reading it back is one word per 64 entries. Both alternatives were measured and rejected: a min..max span is nearly the whole buffer, since a frame's changes land in 5-20 scattered runs, and a Vec of indices would mean an allocation and a sort per frame at several thousand marks. It replaces Primitives::updated -- one bool that covered the instances and the per-primitive data together, so rewriting a rect's region re-uploaded every glyph -- and TrackedArena::changed. The trap only the rig could catch: writing an entry is not changing it. Recycling rewrote every glyph of every moved row with identical bytes, marking 73% of the glyph array against 0.6% genuinely changed, because what moves is the instance's region and not the glyph. PrimitiveVec::set and Primitives::set_instance compare before marking. Every array now uploads within a hair of its floor: fling instances 3.4% against 3.3%, fling glyphs 0.9% against 0.8%, stream glyphs 0.6% against 0.6%. Stream instances are at 72.7%, which *is* the floor and is a layout question rather than an upload one -- the list is pinned to the newest end, so a growing reply moves every row, and that should be one move_offsets write rather than a redraw. Noted in RUST.md as the next thing. Also: draw_inner's four old_* parameters become one Retained struct, so the recycle pool is a field rather than an eleventh positional argument next to three others of the same shape; and free_primitive is the one place a slot and its draw-order position are retired together. The rigs move to scripts/rigs/ui-profile, a crate of their own so a rig's dependencies stay out of the app's -- arena_churn needs bytemuck, which nothing in ai-app does. arena_churn prints floor, uploaded and whole side by side per array, because any two of those alone are misleading and the 122x over-marking above was invisible until all three were on screen together. |
||
|
|
42d54eec95 |
Fling: the vsync clock, the frame ask, and a report that can say what it measured
Iris, from her phone: "some stuttering when flinging in particular. Harder to notice with my finger directly moving the scroll." Her fling phase was 103fps on a 120Hz screen at p50 6.3ms. Two of the four things found are corrections to the instrument, not the renderer. The swapchain acquire -- `get_current_texture`, which *blocks* until the compositor frees an image -- was inside the span the report called iris's CPU work, so a fling comfortably ahead of the display read as milliseconds of being slow. A frame is now three measured parts (`FrameParts`: build, acquire, submit), per phase as well as per run. And nothing could say a frame was never *produced*: `late` counts frames that cost too much, which a reader does not see, while a frame that never happens leaves the last one up for two refreshes, which is the stutter. `PhaseStats::missed` counts vsyncs nothing was drawn for. It closes on the emulator: 1548 frames + 452 missed over 33.0s at 60Hz is 1980 vsyncs. The other two are the frame loop. `Choreographer.postFrameCallback` schedules for the next vsync after the call, and iris asked at the *end* of the callback -- so any frame whose work ran past the boundary registered too late and got the vsync after, one frame over budget silently costing a second. It is asked for immediately after `tick_animations` now, on both backends. And the fling was advanced on `Instant::now()` rather than the vsync `do_frame` carries: frames are presented on an even cadence whatever clock computes them, so sampling the spline at "whenever the callback ran" moves the content unevenly with no frame late enough to appear in any report -- and a drag never had it, which is the asymmetry Iris described. `PointerClock` is `DeviceClock` and the view keeps one, anchored by whichever of a touch or a frame comes first, so a fling is advanced on the clock its velocity was measured on. `opt-level` for the Android release build goes from "s" to 3. The table in RUST.md picked "s" on bytes alone; over the same warm fling eight times iris's own per-frame work is p90 0.15ms/p99 0.42ms at "s" against p90 0.09ms/p99 0.26ms at 3, for 1.8 MB of arm64 APK. `app-rust/tests/fling_profile.rs` is the rig that established what a fling frame actually costs and is kept for next time (Iris: "please keep the profiling rig around for future use"): only one frame in six lays anything out, and the multi-millisecond spikes are all first-pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6d5a231f5c |
iris is the framework alone; the app is one crate in app-rust/
Iris: "the organization of the rust rewrite is a mess right now... there shouldn't be anything related to the app inside of iris. Iris is supposed to be the UI framework alone." And, on the crate count: "I'm confused why the app only code needs more than one crate though." Nine cargo workspaces become three, and the port's project code -- which sat in five places, four of them inside the framework -- becomes one crate, `ai-app`, in `app-rust/`: client-core -> app-rust/src/client iris/transcript-ui -> app-rust/src/ui iris/transcript-fixture -> app-rust/src/ui/fixture.rs + tests/ + touch/ iris/desktop-app -> app-rust/src/desktop + src/bin_desktop.rs iris/android-app -> app-rust/src/android + android-project/ android-shell -> app-rust/src/shell iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now mentions no session, transcript, setup or server anywhere. Only two of the old splits had a reason that survived reading. event-model stays a crate at the repo root because server/ depends on it too, so a crate is what makes the backend and the app agree by construction. The two Android .so names looked like a hard constraint -- a package produces one library artifact -- until P2 turned out to already plan merging those two Android apps into one; both faces now come out of libai_app.so, picked apart by features so `--no-default-features --features shell` keeps wgpu, parley and iris out of the Compose app's APK. docs/RUST.md's "One app crate" has the rest, including what each remaining feature is for. DECISIONS.md and SUBAGENTS.md move into docs/ with everything else. Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so, build-apk.sh produces an APK that installs and launches on this checkout's emulator (Gl ... virgl, as expected), and the phone-sized headless screenshot renders the transcript unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |