Prune commentary and stale Rust port notes

This commit is contained in:
iris committed 2026-09-10 00:44:13 -04:00
1 parent 3ae034a47b
commit 1e6d3b1edd
84 files changed
+334 -5648

No files matched your search

-251
View File
@@ -11,10 +11,6 @@ use android_view::{
},
ndk::event::{Axis, Keycode, MotionAction},
};
// `marker::Sized` explicitly: `crate::prelude::*` below also brings in the
// `Sized` *widget* (`widget::position::sized::Sized`), and an unqualified
// glob import shadows the language prelude -- `default/mod.rs` has the same
// explicit import for the same reason.
use std::{
cell::RefCell,
marker::{PhantomData, Sized},
@@ -62,43 +58,9 @@ pub struct AndroidUiState {
/// path -- see `android/insets.rs` for why they need a registry of
/// their own.
shared: Rc<RefCell<Shared>>,
/// I4 (RUST.md): pushed from `IrisViewPeer::render` and consulted by
/// the `AccessibilityNodeProvider` impl below; see `android/access.rs`
/// for the abort mitigation every `raise` on it goes through.
pub access_adapter: AccessAdapter,
/// The AccessKit tree itself -- see `iris_core::AccessTree`'s doc
/// comment.
pub access: AccessTree,
/// iris's own frame-time report (RUST.md's I5 box, "Measurements
/// taken" (b)) -- `render()` below records into it once per frame,
/// because `dumpsys gfxinfo` cannot see a `SurfaceView`'s own
/// GPU-drawn frames at all. See `iris_core::FrameReport`'s own doc.
pub frame_report: FrameReport,
/// `DisplayMetrics.density` (`new_peer`'s doc comment): physical pixels
/// per dp on this device, read once at view construction and carried
/// on `UiRenderState::density` (`render.set_density`, `new_peer`) from
/// then on -- every `Len::dp` in the widget tree resolves against it at
/// layout time (`Len::dp`'s field doc, IRIS_TODO.md's
/// "density-independent length unit" item, 2026-09-06).
///
/// **Everything else in this module is physical pixels, matching the
/// real wgpu surface/swapchain resolution** -- window size, touch
/// coordinates, insets. That is a correction from an earlier version
/// of this comment, which had `window_size`/`surface_changed`'s
/// `UiRenderState::resize` call divide by `content_scale` into a
/// *logical* coordinate space instead, as a global stopgap for
/// RUST.md's P0 box's phone report ("text is far too small"). That
/// stopgap fixed the size but not the *sharpness*: dividing to logical
/// units meant a `16.0`-sized glyph rasterised at 16 physical px and
/// then implicitly upscaled ~3x by the NDC mapping onto the real
/// physical framebuffer -- the exact "blurry ... glyphs drawn at
/// logical size and stretched by the scale" Iris reported next.
/// Resolving `dp` at layout time replaces it: a widget author writes
/// `dp(16)` for a size that should look the same physical size on any
/// density, and everything downstream (layout, hit-testing, the window
/// uniform, and the font size handed to the text shaper) works in the
/// display's own physical pixels throughout, so nothing is
/// rasterised at one resolution and displayed at another.
pub content_scale: f32,
/// The last insets `render()` saw -- compared each frame so
/// `AndroidAppState::on_insets_changed` fires only when they actually
@@ -131,13 +93,6 @@ impl AndroidUiState {
self.shared.borrow().insets
}
/// The insets state as one line for a diagnostics pane, including how
/// many times the platform has delivered any -- see
/// `insets::Shared::updates` for why the count is the load-bearing
/// part. `dispatches=0` says the listener has never run and the
/// numbers beside it are defaults rather than measurements, which is
/// the distinction a screenshot otherwise cannot make (UI_RULES.md,
/// "design the unknown state first").
pub fn insets_report(&self) -> String {
let shared = self.shared.borrow();
let i = shared.insets;
@@ -167,39 +122,12 @@ pub trait HasAndroidUiState: Sized + 'static {
pub trait AndroidAppState: HasAndroidUiState {
fn new(ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self;
/// The system back gesture/button. `true` means handled -- nothing
/// further happens; `false` lets the activity finish as it would with
/// no view at all. The default declines, since most screens have
/// nothing to intercept it for.
#[allow(unused_variables)]
fn back_pressed(&mut self, rsc: &mut AndroidRsc<Self>, render: &mut UiRenderState) -> bool {
false
}
/// Called once, right after `new`, with a fresh `JavaVM` handle and a
/// global reference to this app's own `View` -- for a caller that
/// needs to call into Java itself beyond what a [`RequestRedraw`]
/// handle already covers (P0's bench build calling
/// `BatteryManager`/`ClipboardManager` through the view's `Context`,
/// docs/RUST.md). Not folded into `new` itself: most implementors need
/// nothing here, and `new`'s job is building the widget tree, not
/// holding a platform handle -- the default does nothing. `vm`/`view`
/// are independent handles from the ones `new_peer` keeps for its own
/// `RequestRedraw` (a fresh `get_java_vm`/`new_global_ref` each), so
/// storing them has no effect on that mechanism.
#[allow(unused_variables)]
fn platform_ready(&mut self, rsc: &mut AndroidRsc<Self>, vm: JavaVM, view: GlobalRef) {}
/// Called from `render()` whenever `AndroidUiState::insets()` differs
/// from what it was last frame -- once at startup for the status bar
/// (RUST.md's P0 box: "the status-bar inset is not applied" reported
/// the two top buttons sitting under it, because nothing read `.top`
/// at all), and again on a rotation or the keyboard opening/closing.
/// `insets` is in the same physical-pixel units everything else in the
/// tree now uses (`AndroidUiState::content_scale`'s field comment), so
/// a widget can add it to a layout size directly -- `dp(...) +
/// abs(insets.top)` if the widget wants a density-independent size
/// plus the system bar's own (already-physical) height. The default
/// does nothing -- most screens have no chrome that sits under a
/// system bar.
#[allow(unused_variables)]
fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: WindowInsets) {}
}
@@ -238,11 +166,6 @@ impl WindowInsets {
}
}
/// The android-view analogue of `default::DefaultRsc` -- identical in
/// substance, since none of `UiRsc`/`HasEvents`/`HasTasks`/`HasWidgetState`
/// mention winit. Kept as a separate type rather than shared code because
/// the two backends' `ViewPeer`/`ApplicationHandler` entry points hold
/// their harness state differently (see RUST.md's I2).
pub struct AndroidRsc<State: 'static> {
pub ui: UiData,
pub events: EventManager<Self>,
@@ -379,11 +302,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) {
self.run_input_frame(ctx);
// RUST.md's P0 box, "doesn't enter it until I hit space, and also
// doesn't move cursor forward": Gboard needs `updateSelection`
// after every edit to keep its own model of the field in sync, or
// it holds keystrokes back rather than trusting a screen it
// believes is stale. See `update_ime_selection`'s own doc.
self.update_ime_selection(ctx);
let ui_state = self.state.android_state_mut();
@@ -393,16 +311,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
}
}
/// The view's one clock, anchoring it on `nanos` if nothing has yet
/// -- see the `device_clock` field. Either source may be the first to
/// arrive: a frame callback fires before any touch on an app that
/// animates at startup, and a touch arrives first on one that does
/// not.
///
/// `oldest` is the earliest sample the anchoring event carries, which
/// matters only when this is the call that anchors -- see
/// `DeviceClock::anchored`. A frame time carries no batch, so it
/// passes its own time for both.
fn device_clock(&mut self, event_time: i64, oldest: i64) -> DeviceClock {
*self
.device_clock
@@ -417,20 +325,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
}
}
/// The `log::debug!` calls here are a live diagnostic for a still-open
/// finding (RUST.md's I2): layout runs and reports the right pixel
/// region for the root (confirmed via `window_region`, logged below),
/// and the clear colour reaches the screen (confirmed by swapping it to
/// magenta and screenshotting), but no primitive ever appears on top of
/// it -- on both the Vulkan/SwiftShader and GLES/virgl backends. Leave
/// these in until that is root-caused; removing them loses the exact
/// evidence a `logcat` capture needs to reproduce the state. Gated on
/// `iris::diagnostics::trace_enabled` since 2026-09-07 (docs/RUST.md's
/// review, D1): unconditional, they were two `debug!` lines every
/// rendered frame, and `client_core::log_ring`'s `RingLogger` records
/// every level the app's already-`Debug` install lets through
/// regardless of target, so they filled the whole ring in under ten
/// seconds at 120Hz and left `Copy report` nothing else to show.
fn render(&mut self, ctx: &mut CallbackCtx, now: Instant) {
if self.state.android_state().renderer.is_none() {
return;
@@ -444,12 +338,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
let current_insets = ui_state.insets();
if current_insets != ui_state.last_insets {
let physical = WindowInsets::from_physical(current_insets);
// One line per real insets change. Iris's phone is the only
// place several of these bugs reproduce and `adb logcat` is
// the only instrument there (this-machine-android: system
// tracing is broken on that device), so the numbers a layout
// is actually fed have to reach the log -- "the composer
// floats at launch" is unanswerable from a screenshot alone.
log::info!(
"iris insets: left={} top={} right={} bottom={} ime_bottom={} \
ime_visible={} window={:?}",
@@ -465,14 +353,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
self.state.on_insets_changed(&mut self.rsc, physical);
}
// Gated the same way `iris::frame`'s own line is (docs/RUST.md's
// "Phone logging" review, D1): a bare `log::debug!` reaches
// `client_core::log_ring`'s ring regardless of level, since
// `RingLogger::enabled` is unconditionally `true` and the app
// installs at `LevelFilter::Debug` -- two of these a rendered
// frame filled the 2000-line ring in under ten seconds at 120Hz,
// leaving `Copy report` nothing but frame spam. See
// `iris::diagnostics`'s module doc.
if crate::diagnostics::trace_enabled() {
let ui_state = self.state.android_state();
log::debug!(
@@ -488,35 +368,8 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
self.window_size(),
);
}
// iris's own frame-time report (RUST.md's I5 box, "Measurements
// taken" (b)): started here, at the same point a redraw request
// fires, and stopped after `renderer.draw()`'s `queue.submit` +
// `present()` -- the span Compose's render report and `gfxinfo`
// both count. See `iris_core::FrameReport`'s own doc for exactly
// what this does and does not measure.
let frame_start = Instant::now();
// Anything moving on its own -- today a `LazySpan` coasting
// through a fling -- is advanced here, before the draw. **On
// `now`, not on `frame_start`**: `now` is the vsync the
// `Choreographer` handed this callback, which is evenly spaced,
// while `frame_start` is whenever the callback actually got to
// run. The frames are *presented* on the even cadence either way,
// so sampling the animation on the uneven one moves the content by
// an uneven distance per frame -- a fling that shimmers with no
// frame late enough to show up in a report. See
// `UiData::tick_animations` and `sense::DeviceClock`;
// `default/mod.rs`'s `RedrawRequested` arm is the winit half.
let animating = self.rsc.ui.tick_animations(now);
// **Asked for before the work, not after it.** A frame callback is
// one-shot, so an animation that wants another frame has to say so
// every frame -- and `Choreographer.postFrameCallback` schedules
// for the next vsync *after the call*. Asking at the end of this
// function meant any frame whose work ran past the vsync boundary
// (the swapchain acquire below alone can sit most of a frame)
// registered too late for the next one and got the one after --
// so one frame over budget silently cost a second frame as well.
// Unlike `after_input`, which only has to ask when input dirtied
// something.
if animating {
ctx.view.post_frame_callback(&mut ctx.env);
}
@@ -527,14 +380,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
return;
};
let frame_diagnostics = renderer.update(&mut self.rsc.ui, &mut self.render);
// First `DIAGNOSTIC_FRAMES` frames after each `surface_changed`
// only -- RUST.md's P0 box, "the first input frame" investigation:
// the glyph-wipe Iris reported happens on the first tap or scroll
// after a fresh surface, so a report from that window is what
// would show whether an atlas grow, a masks/move_offsets resize, or
// a fresh wgpu error coincided with it. `frame_count()` was just
// incremented inside `update()`, so `<=` counts frame 1 through
// `DIAGNOSTIC_FRAMES` inclusive.
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
log::info!(
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
@@ -549,9 +394,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
}
let mut parts = renderer.draw();
parts.total = frame_start.elapsed();
// Dated on `now` -- the vsync this frame was for -- so the gap
// between consecutive frames is the display's own cadence and
// `PhaseStats::missed` counts vsyncs nothing was drawn for.
self.state
.android_state_mut()
.frame_report
@@ -570,12 +412,6 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
);
}
// I4 (RUST.md): only produces a `TreeUpdate` -- and so only queues
// anything to raise -- when the named set actually changed this
// frame; see `AccessTree`'s doc comment. Deferred rather than
// raised inline so it runs after this callback releases whatever
// it's holding, matching android-view's own demo and `raise`'s own
// contract.
let ui_state = self.state.android_state_mut();
if let Some(tree_update) =
ui_state
@@ -597,19 +433,6 @@ fn show_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) {
imm.show_soft_input(env, view, 0);
}
/// Replaces the activity's content with a plain, selectable, scrollable
/// text view holding `report` -- the on-screen half of `surface_changed`'s
/// renderer-failure path (UI_RULES.md: "a failure is reported where it
/// happened, and says what to do next," here "copy this and send it").
/// Goes through an ordinary instance method on the Java side
/// (`IrisView.showRendererError`) rather than a new `native` method: this
/// call is Rust reaching *into* Java, the opposite direction from every
/// `native fn` android-view/`IrisView` declare, and an ordinary virtual
/// call resolves against `ctx.view`'s real runtime class (`IrisView`) the
/// same way any other JNI method call here does. Silently does nothing on
/// any JNI failure -- there is no more-fallback screen to fall back to,
/// and the `log::error!` in `surface_changed` already reached logcat
/// first.
fn show_renderer_error<'local>(env: &mut JNIEnv<'local>, view: &View<'local>, report: &str) {
let Ok(message) = env.new_string(report) else {
return;
@@ -703,18 +526,6 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
let trace_input = crate::diagnostics::trace_enabled();
let mut historical_ms: Vec<(u64, f32, f32)> = Vec::new();
// **Historical samples first.** A flick on a 120Hz screen is
// delivered as one or two `MotionEvent`s with the intermediate
// positions batched inside them, so reading only `x()`/`y()` threw
// away every sample but the last: the velocity tracker saw one
// `Pan` for the whole gesture, `VelocityTracker::velocity` answers
// 0.0 below two samples, and the release therefore flung at zero --
// Iris's phone, twice ("fling still doesn't work"), while a
// `ui-trace` swipe, which is many evenly-spaced events, flung fine.
// Replayed one at a time through the sensors rather than summarised,
// so the arbiter, the tracker and any other sensor all see the same
// motion the finger actually made; only the last sample ends the
// frame (`after_input`).
if matches!(action, MotionAction::Move) {
// Android documents the historical samples as oldest first and
// the event's own sample as the newest of the batch; everything
@@ -757,15 +568,6 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
ui_state.cursor.pos = vec2(x, y);
ui_state.cursor.buttons.left.update(false);
}
// A cancel ends the press -- a release that never arrives
// leaves whichever widget took pointer capture holding it
// forever -- but it is **not** a release, and saying so is
// `CursorState::cancelled`. It used to take the `Up` arm, so
// the system's own swipe up from the bottom edge to leave the
// app (moves, then `ACTION_CANCEL`) reached iris as a flick
// released at speed, and the transcript flung while the app
// was in the background: Iris's 2026-09-08 "leaving and
// reopening the app also randomly moved the vertical scroll".
MotionAction::Cancel => {
ui_state.cursor.pos = vec2(x, y);
ui_state.cursor.buttons.left.update(false);
@@ -837,25 +639,6 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
// in particular) mean something different from a `rest`-based one.
self.render.resize((width as f32, height as f32));
// **Reuse the existing renderer (device, atlas, buffers, bind
// groups) when one is already live -- only reconfigure the
// surface.** `surfaceChanged` fires on *every* size or format
// change, not only on a genuinely new `Surface`/window: showing
// the IME under `adjustResize` resizes the same `SurfaceView` and
// is reported through this exact callback. Rebuilding the whole
// `AndroidRenderer` here used to mean a fresh `UiRenderNode::new`
// -- a brand-new, empty glyph atlas and fresh GPU buffers -- while
// `iris_core`'s CPU-side glyph cache (`primitive/text.rs`) kept the
// atlas coordinates it had already handed out against the *old*
// atlas. Every glyph then drew from a UV rectangle that pointed
// into a texture that had just been recreated empty, so text
// vanished on the first keyboard open while rects (which never go
// through the atlas) kept drawing -- exactly the "rectangles stay,
// glyphs disappear" Iris reported. Confirmed by reading this path
// end to end (no fresh-atlas rebuild anywhere in `resize()` below,
// only in `AndroidRenderer::new`) before changing anything, per
// AGENTS.md's "verify before finishing".
//
// `AndroidRenderer::resize` only reconfigures the wgpu surface and
// rewrites the window uniform -- device, atlas, buffers and bind
// groups are untouched, so the glyph cache's coordinates stay
@@ -881,17 +664,6 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
}
let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env);
// `AndroidRenderer::new` used to panic here through wgpu's own
// default uncaptured-error handler on a bind-group-layout
// validation failure -- exactly what aborted the P0 bench APK on
// Iris's phone with the message truncated to "wgpu error:
// Validation Error" and nothing else recoverable from the crash
// report (RUST.md's P0 box, "iris bench crash on the phone,
// 2026-09-06"). It now returns the full diagnostic instead; this is
// the one place in the app that can turn it into something a
// person can read, since `ctx.view`/`ctx.env` (needed to reach the
// Java side) are only in scope inside a `ViewPeer` callback.
//
// `content_scale` reaches `AndroidRenderer` only for the
// Diagnostics page's report text now -- window size and the
// shader's window uniform are physical pixels throughout (see the
@@ -930,24 +702,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.render(ctx, Instant::now());
}
Err(report) => {
// One line for logcat (UI_RULES.md: "the full text for
// whoever can read the log" lives here), the multi-line
// original on screen -- `show_renderer_error` below.
log::error!("iris renderer init failed: {}", report.replace('\n', " | "));
// Deferred, not called directly: `Activity::setContentView`
// tears the old view hierarchy down synchronously, which
// fires `IrisView`'s own `onFocusChanged` before
// `setContentView` returns -- straight back into this same
// `IrisViewPeer` through `on_focus_changed` while
// `with_peer` (android-view's dispatch, `view.rs` upstream)
// still holds this peer's `RefCell` borrow for the
// `surface_changed` call in progress. Found by inducing a
// validation error and hitting `RefCell already borrowed`
// at exactly that reentrant call (RUST.md's P0 box).
// `push_dynamic_deferred_callback` runs after `with_peer`
// drops the borrow, which is what every other callback in
// this file that reaches into Java already relies on
// (`raise_if_enabled`, above).
ctx.push_dynamic_deferred_callback(move |env, view| {
show_renderer_error(env, view, &report);
});
@@ -996,8 +751,6 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
/// one.
fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {
self.drain_tasks();
// No vsync to date this one on -- it is a background task's
// "there is new state", not a frame the display asked for.
self.render(ctx, Instant::now());
}
@@ -1106,8 +859,6 @@ pub fn new_peer<'local, State: AndroidAppState>(
state: Default::default(),
_state: PhantomData,
};
// See `TextData::density`'s field doc for why this is set alongside
// `render.set_density` below rather than read from there.
rsc.ui.text.density = content_scale;
let shared = Rc::new(RefCell::new(Shared::default()));
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
@@ -1116,8 +867,6 @@ pub fn new_peer<'local, State: AndroidAppState>(
let platform_view = env.new_global_ref(&view.0).unwrap();
state.platform_ready(&mut rsc, platform_vm, platform_view);
let mut render = UiRenderState::new();
// Every `Len::dp` in the tree resolves against this from now on -- see
// `UiRenderState::density`'s field doc and `Len::dp`'s.
render.set_density(content_scale);
let peer = IrisViewPeer {
rsc,