Files
ai-app/docs/RUST.md
T

9.0 KiB

Rust app and Iris

The production app is one Rust crate under app/, with shared Android and desktop UI drawn by the iris framework pinned under iris/ as a submodule. 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 screens; only a benchmark build adds 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 iris/TODO.

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
./scripts/run-headless.sh phone --phone --dir ../app --shot /tmp/phone.png
./scripts/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/scripts/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.