Files
ai-app/docs/RUST.md
T

201 lines
8.9 KiB
Markdown

# Rust app and Iris
The production app is one Rust crate under `app/`, with shared Android and
desktop UI drawn by the in-tree `iris` framework. This document keeps the
open work and measured constraints that are expensive to rediscover.
## Boundaries
- `app/src/client` contains transport, config, caching, folding, highlighting,
and other platform/UI-independent logic. It must not depend on Iris.
- `app/src/ui` contains product screens and reusable product widgets.
- `app/src/android` and `app/src/desktop` contain only platform integration.
Platform services are exposed to shared UI through small traits.
- `iris/` is only a UI framework. Session, transcript, server, setup, and
product styling concepts never belong there.
- `iris/src/rsc` groups resources carried by or installed through `StdRsc`:
attributes, events, overlays, pointer sensing/input, tasks, and per-widget
state. It is the standard bundle used by Iris's hosts, not a requirement;
shared widgets depend on its narrow resource traits so a custom bundle can
implement only what it needs. `widget/` is reserved for concrete retained
widgets. Public paths such as `iris::sense` remain aliases for readability.
- Phone and desktop layouts may differ. Their component implementations,
colors, spacing, text styles, gestures, folding, and network behavior do
not.
The application crate has an rlib for tests and desktop and a cdylib for
Android. Features select optional surfaces: `screens` links Iris, `fixture`
embeds the synthetic transcript, and `bench` adds the benchmark runner. An
ordinary APK uses `transcript-screen`, not `bench`.
## Current state
The transcript screen runs in the headless harness, a desktop window, the
Android emulator, and a real phone. The phone performance gate passed; the
preserved reports are under `docs/bench/`.
Open session-screen work:
- History paging, viewport-sized prefetch, distinct unknown/empty/error
states, and jump-to-latest.
- Images, attachments, session settings, usage display, and dialogs.
- Re-verification of IME/inset behavior on the real phone.
Later product work:
- Root navigation and the sessions/import/models/setups surfaces.
- File explorer and editor, including conflict handling.
- Settings, notification permission, sharing, and notification service.
- Desktop layouts for those surfaces.
Client route coverage and gaps live in `docs/CLIENT_CORE.md`; framework work
lives in `docs/IRIS_TODO.md`.
## Test layers
Use the cheapest layer that can answer the question.
### 1. In-process harness
`iris::harness` builds the real widget tree with no window, compositor, or
GPU. Its clock is controlled by the test. `TouchScript` replays the same
historical samples Android supplies. Recordings live under `app/touch/`.
cd app && cargo test
The suite covers fling velocity and duration, catching a moving list,
cancellation, taps, long-press selection, list bounds, and composer insets.
It cannot answer pixel, atlas, GPU upload, surface lifecycle, JNI, clipboard,
or real-inset questions.
`iris/tests/mask_sdf.rs` is the narrow exception: it uses a headless GPU
compute pass to prove the shader and CPU hit-test arithmetic agree.
### 2. Phone-shaped desktop window
cd iris
./run-headless.sh phone --phone --dir ../app --shot /tmp/phone.png
./run-headless.sh phone --phone --dir ../app \
--replay ../app/touch/flick-120hz.touch --shot /tmp/replay.png
This layer is for visual output and desktop input integration. Debug frame
times on the VM's GPU are not phone measurements.
### 3. Android
cd app
./build-apk.sh debug --abi x86_64
Android is required for JNI, lifecycle, IME, insets, clipboard, accessibility,
and renderer fallback. Read the `this-machine-android` skill first. Use a
release arm64 build for real-phone measurements.
The emulator is deliberately a GLES rig: Vulkan is SwiftShader, while GLES
is host-accelerated through virgl. Do not compile with `force-gles`; Iris's
runtime fallback must make the same decision the shipped binary makes. Read
the `iris renderer:` log before interpreting any result.
## Performance rigs and established results
`scripts/rigs/ui-profile/tests/frame_profile.rs` measures the CPU half of a
frame. Run it only in release mode:
cd scripts/rigs/ui-profile
cargo test --release --test frame_profile -- --ignored --nocapture
A fling is not CPU-bound after the first pass over content: only roughly one
frame in six performs layout, and later passes measured p99 0.26 ms. Initial
multi-millisecond spikes are text shaping for newly visited transcript rows.
A streamed event originally measured 3.86 ms in the host rig and 9.5 ms on
the phone. Folding was 0.35 ms and applying the widget diff 0.41 ms; shaping
dominated because the old fixture grew one 14,888-character markdown block.
Real replies measured a largest block of 1,580 characters over 7,706 blocks.
The fixture now inserts blank lines and retains the run-on block only as a
one-shot stress case.
`arena_churn.rs` measures GPU-array upload by reporting the byte-diff floor,
the bytes Iris uploads, and the former whole-array cost. The rig established:
- Whole-array writes cost 758 MB over one fling and 1.2 GB over 401 streamed
deltas.
- `ArrBuf` must retain allocation across length changes.
- Redraw must reuse primitive slots within the frame rather than freeing and
appending them.
- Dirty ranges must track changed entries and cancel provisional writes that
are restored before upload.
- `Painter::set_child_offset` and stable `LazySpan` row boxes reduce pinned
growth to a 1.1% instance upload, matching its 1.1% floor. Median instance
upload fell from 176,496 bytes to 1,488 bytes.
Do not optimize folding again without new evidence. The measurements ruled it
out as the streamed-frame bottleneck.
## Text and fonts
Body and monospace text come from platform fonts. Icons use the bundled Nerd
Font subset because their codepoints are a small closed set no platform must
provide. Android font enumeration previously rendered some bold spans as
blank gaps, so every real-phone build must visually verify bold text and the
triangle disclosure glyphs; desktop and emulator font sets are not evidence
for the phone.
Markdown is split into blocks once and retained. A delta reshapes only the
block it changes. Tables wrap cells and horizontally scroll only after their
minimum useful column width. Links share a text widget and hit regions; they
must not become one layout node per link.
## Scrolling and retained rendering
`docs/SCROLL.md` defines scrolling. One `ScrollController` owns position,
gesture, fling, and pin state. `ScrollArea` and `LazySpan` each own one through
`Scrollable`.
The transcript is bottom-anchored. Appending while pinned keeps the newest
edge fixed; loading history above the anchor must not move visible content.
Expanding a row holds the edge nearest the tap. Retained child offsets move a
subtree without rewriting every primitive.
A page is a number of events, not a number of visible rows: many deltas fold
into one reply and many tool calls can group into one row. Prefetch therefore
uses measured viewports, never a fixed row count.
## Android host
The Java activity owns the Android lifecycle, the `View`, enrollment intent,
and insets. `iris::android` owns surface lifecycle, touch history, generic
motion, hover, mouse buttons, wheel input, IME plumbing, and accessibility.
The product client consumes the same Iris pointer state as desktop.
Static inset dispatch supplies settled values; `WindowInsetsAnimation`
supplies intermediate IME heights and re-reads settled insets at animation
end. Visibility and height are separate signals during animation. targetSdk
35+ is required for reliable IME overlap under enforced edge-to-edge.
The application id is `com.example.aiapp`, preserving the retired app's
identity. The Java package remains `dev.iris.android.demo`, an internal JNI
name. Explicit benchmark builds add `.bench` to the application id.
## Build constraints
- `app`, `iris`, and `scripts/rigs/ui-profile` use synchronized rolling
nightly toolchains.
- Android uses the `android-release`/`android-dev` profiles so phone settings
do not affect desktop builds.
- `app/build-apk.sh` removes prior ABI native outputs before packaging; Gradle
otherwise may reuse a stale merged library from another ABI.
- Release builds are signed by the machine-local
`~/.config/ai-app/release.jks`. No key or enrollment secret belongs in the
repository.
- `iris/run-headless.sh --dir DIR` selects the workspace containing the
example and defaults to `iris/`.
## Session-screen pass condition
Use `app/ui-sandbox.sh` with artificial delay, a large transcript, and slow
spawn. Exercise unknown, empty, error, and too-long states. Drive controls by
accessibility label. Verify paging seams, jump-to-latest, selection, tool-card
state, message steering, and IME closure. Then re-run the frame report on the
real phone and compare only with the same Iris reporter—not historical Compose
timings, whose clocks and accounting differ.