Compare commits
3
Commits
203f53470c
...
50e69995b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50e69995b6 | ||
|
|
85869d02f8 | ||
|
|
f99ae4c366 |
No files matched your search
@@ -8,6 +8,28 @@ capability that moved. Small and trivial changes do not go here.
|
|||||||
An entry gives the date, what changed, why, and a short before/after where
|
An entry gives the date, what changed, why, and a short before/after where
|
||||||
it helps judge the change without the session that made it. Newest first.
|
it helps judge the change without the session that made it. Newest first.
|
||||||
|
|
||||||
|
## 2026-09-07: iris runs on a GLES-only Android device, and reports the renderer it cannot build
|
||||||
|
|
||||||
|
`AndroidRenderer::new` asked wgpu for `Backends::PRIMARY`, which does not
|
||||||
|
include `GL`. A device that offers a Vulkan driver with no adapter behind
|
||||||
|
it -- this checkout's emulator -- therefore had no adapter at all, and the
|
||||||
|
`.expect` on that turned into a crash loop with nothing on screen. It now
|
||||||
|
probes for a `PRIMARY` adapter first and falls back to `Backends::GL` when
|
||||||
|
there is none, so **Vulkan still wins wherever it has an adapter** and
|
||||||
|
nothing changes on a phone.
|
||||||
|
|
||||||
|
The probe deliberately runs on an instance that never touches the window:
|
||||||
|
an Android window can be connected to one graphics API only, so an
|
||||||
|
instance carrying both backends lets Vulkan claim the window and leaves
|
||||||
|
the GLES surface unusable. That is why this is a second instance rather
|
||||||
|
than one wider `Backends` value.
|
||||||
|
|
||||||
|
The other half a caller sees: `AndroidRenderer::new` already returned
|
||||||
|
`Result<Self, String>`, and now **every** way it can fail goes through
|
||||||
|
that -- no surface, no adapter, no device, as well as the bind-group
|
||||||
|
validation failure it was originally written for. `surface_changed` puts
|
||||||
|
that string on screen and in the log ring instead of aborting.
|
||||||
|
|
||||||
## 2026-09-07: `VelocityTracker` takes positions, not deltas
|
## 2026-09-07: `VelocityTracker` takes positions, not deltas
|
||||||
|
|
||||||
A flick released at the wrong speed because the tracker averaged. It now
|
A flick released at the wrong speed because the tracker averaged. It now
|
||||||
|
|||||||
+60
-5
@@ -716,11 +716,66 @@ closes it.
|
|||||||
devlog agent shrank the label type 18 -> 13 to make room (UI_RULES:
|
devlog agent shrank the label type 18 -> 13 to make room (UI_RULES:
|
||||||
never shrink text to fit). Put the controls in two rows or make the
|
never shrink text to fit). Put the controls in two rows or make the
|
||||||
header wrap; restore the size. Sonnet.
|
header wrap; restore the size. Sonnet.
|
||||||
- [ ] Bench app crash-loops on this checkout's emulator once
|
- [x] **Bench app crash-loops on this checkout's emulator (done
|
||||||
backgrounded (seen by the devlog agent, pre-existing; the phone's
|
2026-09-07).** Not the surface lifecycle at all, and not "once
|
||||||
resume works since ba2afba). Reproduce on the emulator's GLES path,
|
backgrounded" -- a build with the **default features** (no
|
||||||
read the crash from the devlog/`adb logcat`, fix. Opus if it is in
|
`force-gles`) never got a first frame. `AndroidRenderer::new`
|
||||||
the surface lifecycle.
|
(`iris/src/android/render.rs`) asked wgpu for `Backends::PRIMARY`,
|
||||||
|
which does not contain `GL`, and this emulator advertises a Vulkan ICD
|
||||||
|
with no adapter behind it: `RequestAdapterError::NotFound {
|
||||||
|
active_backends: VULKAN, no_adapter_backends: VULKAN,
|
||||||
|
supported_backends: VULKAN | GL }`, `.expect`ed, so SIGABRT, so the
|
||||||
|
launcher restarts it -- the loop. A `force-gles` build was never
|
||||||
|
affected, which is why the crash looked like it belonged to whatever
|
||||||
|
else was going on. **Root cause: iris refused a device whose only
|
||||||
|
usable adapter is a GLES one.**
|
||||||
|
|
||||||
|
**Fix, two halves.** (1) `AndroidRenderer::new` now probes for a
|
||||||
|
`PRIMARY` adapter with an instance that never touches the window and
|
||||||
|
rebuilds the instance on `Backends::GL` when there is none. The probe
|
||||||
|
is surface-free deliberately: **an Android window can be connected to
|
||||||
|
one graphics API only**, so a single instance carrying both backends
|
||||||
|
fails differently and worse -- Vulkan's
|
||||||
|
`vkCreateAndroidSurfaceKHR` claims the window in `create_surface` and
|
||||||
|
the GLES surface built from the same window then reports `In
|
||||||
|
Surface::configure / Invalid surface`, aborting one frame later in
|
||||||
|
`Surface::get_current_texture_view` ("Surface is not configured for
|
||||||
|
presentation"). That was measured here on the way to the fix, not
|
||||||
|
reasoned about. Vulkan still wins wherever it has an adapter, so
|
||||||
|
nothing changes on the phone. (2) The surface, adapter and device
|
||||||
|
requests all report through the `Result<Self, String>` this function
|
||||||
|
already returns, instead of two of the three panicking -- one rule for
|
||||||
|
the set, and `surface_changed` already puts that string on screen and
|
||||||
|
in the log ring.
|
||||||
|
|
||||||
|
**Evidence**, this checkout's emulator (API 36, x86_64, debug):
|
||||||
|
before, default features aborted on first launch with `Abort message:
|
||||||
|
'Could not get adapter!: NotFound {...}'`; after, `iris renderer: no
|
||||||
|
Backends(VULKAN | METAL | DX12 | BROWSER_WEBGPU) adapter on this
|
||||||
|
device, falling back to GLES` then `new renderer built (Gl)` and
|
||||||
|
frames. Then the cases the fix had no reason to touch, clean on both
|
||||||
|
the default build and a `force-gles` one: two background/return
|
||||||
|
cycles, rotate to landscape and back (`already_live=true`, the reuse
|
||||||
|
branch), a background/return after the rotation, and cold starts.
|
||||||
|
**Vulkan could not be exercised here** -- the probe's own answer is
|
||||||
|
that this emulator has no Vulkan adapter, which is the whole defect;
|
||||||
|
Vulkan remains only testable on Iris's phone.
|
||||||
|
|
||||||
|
**Also landed with it, and worth more than the fix**: a panic hook in
|
||||||
|
`iris/android-app/src/app_log.rs`. Checked first, rather than assumed:
|
||||||
|
under `panic = "abort"` a panic's message reaches the tombstone's
|
||||||
|
`Abort message` and **nothing else** -- not `log`, so not the ring, so
|
||||||
|
not Dev Updater's Runtime tab, which is the only surface Iris has on a
|
||||||
|
phone with no `adb`. The hook writes the message and its location at
|
||||||
|
`error` level, and -- because the ring is memory only and the process
|
||||||
|
is about to die -- also to `last-panic.txt` in the app's private
|
||||||
|
directory, which `set_crash_dir` (called from `nativeSetFilesDir`)
|
||||||
|
replays into the ring at `error` level on the next start and deletes.
|
||||||
|
So a crash loop now explains itself in the Runtime tab of the run
|
||||||
|
that is still up. Verified on the emulator by building the *unfixed*
|
||||||
|
renderer with the hook: `iris panic at .../render.rs:140:14: Could not
|
||||||
|
get adapter!: NotFound {...}` in the ring on the run that died, and
|
||||||
|
`iris app log: the previous run died -- ...` on the next one.
|
||||||
- [ ] Masks with a shape -- docs/LAYOUT.md "Masks with a shape (decided
|
- [ ] Masks with a shape -- docs/LAYOUT.md "Masks with a shape (decided
|
||||||
2026-09-07)". A mask references a primitive already drawn
|
2026-09-07)". A mask references a primitive already drawn
|
||||||
(rect SDF, texture or glyph alpha), chained and multiplied; `.masked()`
|
(rect SDF, texture or glyph alpha), chained and multiplied; `.masked()`
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub fn install(max_level: log::LevelFilter) {
|
|||||||
// The line goes through whatever logger did win.
|
// The line goes through whatever logger did win.
|
||||||
log::warn!("iris app log: a logger was already installed, so there is no ring");
|
log::warn!("iris app log: a logger was already installed, so there is no ring");
|
||||||
}
|
}
|
||||||
|
install_panic_hook();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The process's ring -- what `Copy report` appends, what the diagnostics
|
/// The process's ring -- what `Copy report` appends, what the diagnostics
|
||||||
@@ -63,3 +64,61 @@ pub fn diagnostics_line() -> String {
|
|||||||
};
|
};
|
||||||
format!("{}\n{where_to_read}", ring().summary())
|
format!("{}\n{where_to_read}", ring().summary())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where the panic hook leaves its one line, under the app's private
|
||||||
|
/// directory. Read back and dropped by [`set_crash_dir`] on the next
|
||||||
|
/// start.
|
||||||
|
const CRASH_FILE: &str = "last-panic.txt";
|
||||||
|
|
||||||
|
static CRASH_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
/// Installs a `log`-level panic hook, so a panic's message and location
|
||||||
|
/// reach the ring and `logcat` rather than only the tombstone.
|
||||||
|
///
|
||||||
|
/// **Why this is needed at all**: these builds are `panic = "abort"`
|
||||||
|
/// (`Cargo.toml`), and the default hook writes to `stderr` plus
|
||||||
|
/// `android_set_abort_message` -- the crash report. Iris runs these on a
|
||||||
|
/// phone with no `adb`, so the crash report is exactly the surface she
|
||||||
|
/// cannot read, and an `assert!` that fired said nothing anywhere she
|
||||||
|
/// could see it. Routing it through `log::error!` puts it in front of
|
||||||
|
/// `android_logger` *and* in the ring `devlog`'s provider hands to Dev
|
||||||
|
/// Updater.
|
||||||
|
///
|
||||||
|
/// The ring is memory only, so after an abort the process that holds it
|
||||||
|
/// is gone -- hence the file half. [`set_crash_dir`] replays it.
|
||||||
|
fn install_panic_hook() {
|
||||||
|
let previous = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
let where_at = match info.location() {
|
||||||
|
Some(at) => format!("{}:{}:{}", at.file(), at.line(), at.column()),
|
||||||
|
None => "an unknown location".to_string(),
|
||||||
|
};
|
||||||
|
// `info`'s own `Display` repeats the location and a newline;
|
||||||
|
// the payload alone keeps this to the one line the ring wants.
|
||||||
|
let message = info.payload_as_str().unwrap_or("Box<dyn Any>");
|
||||||
|
let line = format!("iris panic at {where_at}: {message}");
|
||||||
|
log::error!("{line}");
|
||||||
|
if let Some(path) = CRASH_PATH.get() {
|
||||||
|
// Best effort by design: a panic is already the failure, and
|
||||||
|
// failing to record it must not become a second one.
|
||||||
|
let _ = std::fs::write(path, &line);
|
||||||
|
}
|
||||||
|
previous(info);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tells the panic hook where to leave its line, and replays the line a
|
||||||
|
/// previous run left there into the ring before deleting it.
|
||||||
|
///
|
||||||
|
/// Called from `nativeSetFilesDir`, which is the first moment the app's
|
||||||
|
/// private directory is known. The replay is at `error` level and says
|
||||||
|
/// it is from the previous run, so a crash loop shows the reason it is
|
||||||
|
/// looping in the Runtime tab of the run that is still up.
|
||||||
|
pub fn set_crash_dir(dir: &std::path::Path) {
|
||||||
|
let path = dir.join(CRASH_FILE);
|
||||||
|
if let Ok(previous) = std::fs::read_to_string(&path) {
|
||||||
|
log::error!("iris app log: the previous run died -- {}", previous.trim());
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
let _ = CRASH_PATH.set(path);
|
||||||
|
}
|
||||||
@@ -180,7 +180,10 @@ pub extern "system" fn Java_dev_iris_android_demo_MainActivity_nativeSetFilesDir
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
#[cfg(feature = "transcript-screen")]
|
#[cfg(feature = "transcript-screen")]
|
||||||
enrollment::set_files_dir(std::path::PathBuf::from(&dir));
|
{
|
||||||
|
app_log::set_crash_dir(std::path::Path::new(&dir));
|
||||||
|
enrollment::set_files_dir(std::path::PathBuf::from(&dir));
|
||||||
|
}
|
||||||
log::debug!("iris app: files directory is {dir}");
|
log::debug!("iris app: files directory is {dir}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-26
@@ -88,9 +88,10 @@ pub struct FrameDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AndroidRenderer {
|
impl AndroidRenderer {
|
||||||
/// `Err` holds a full, human-readable report -- wgpu's own error text
|
/// `Err` holds a full, human-readable report for **every** way this
|
||||||
/// (`UiRenderNode::new`'s doc comment) plus the adapter identity and
|
/// can fail -- no surface, no adapter, no device, or wgpu's own error
|
||||||
/// the limits/downlevel flags bind-group-layout validation checks
|
/// text (`UiRenderNode::new`'s doc comment) plus the adapter identity
|
||||||
|
/// and the limits/downlevel flags bind-group-layout validation checks
|
||||||
/// against -- rather than the panic wgpu's default error handler would
|
/// against -- rather than the panic wgpu's default error handler would
|
||||||
/// otherwise raise with no caller able to see it. This is what aborted
|
/// otherwise raise with no caller able to see it. This is what aborted
|
||||||
/// the P0 bench APK on Iris's phone with only "wgpu error: Validation
|
/// the P0 bench APK on Iris's phone with only "wgpu error: Validation
|
||||||
@@ -107,29 +108,67 @@ impl AndroidRenderer {
|
|||||||
height: u32,
|
height: u32,
|
||||||
content_scale: f32,
|
content_scale: f32,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
// `force-gles` (RUST.md's I5 "Where iris's frame time goes") swaps
|
// `force-gles` (RUST.md's I5 "Where iris's frame time goes") pins
|
||||||
// the software-Vulkan (SwiftShader) path for GLES/virgl on the same
|
// the build to GLES, to isolate whether the backend itself explains
|
||||||
// build, to isolate whether the backend itself explains the frame
|
// the frame time gap against Compose. `cfg!` rather than a runtime
|
||||||
// time gap against Compose. `cfg!` rather than a runtime switch:
|
// switch: there is no way to hand an env var to an already-launched
|
||||||
// there is no way to hand an env var to an already-launched Android
|
// Android process on this machine (see the feature's doc in
|
||||||
// process on this machine (see the feature's doc in Cargo.toml).
|
// Cargo.toml).
|
||||||
let backends = if cfg!(feature = "force-gles") {
|
//
|
||||||
|
// Otherwise: **Vulkan where it has an adapter at all, GLES where it
|
||||||
|
// has none.** `Backends::PRIMARY` leaves `GL` out, so a device
|
||||||
|
// offering only a GLES adapter had no adapter at all and this
|
||||||
|
// function aborted the process -- this checkout's emulator, whose
|
||||||
|
// Vulkan ICD carries no adapter behind it (`NotFound {
|
||||||
|
// active_backends: VULKAN, no_adapter_backends: VULKAN,
|
||||||
|
// supported_backends: VULKAN | GL }`), and the crash loop in
|
||||||
|
// RUST.md's queue.
|
||||||
|
//
|
||||||
|
// The choice is made *before any surface exists*, with an instance
|
||||||
|
// that never touches the window, because **an Android window can be
|
||||||
|
// connected to one graphics API only**. One instance carrying both
|
||||||
|
// backends does not work: `create_surface` builds a raw surface per
|
||||||
|
// backend, Vulkan's `vkCreateAndroidSurfaceKHR` claims the window
|
||||||
|
// first, and the GLES surface made from the same window then fails
|
||||||
|
// `configure` as lost -- measured here as "In Surface::configure /
|
||||||
|
// Invalid surface" followed by an abort in
|
||||||
|
// `Surface::get_current_texture_view`, "Surface is not configured
|
||||||
|
// for presentation".
|
||||||
|
let mut backends = if cfg!(feature = "force-gles") {
|
||||||
Backends::GL
|
Backends::GL
|
||||||
} else {
|
} else {
|
||||||
Backends::PRIMARY
|
Backends::PRIMARY
|
||||||
};
|
};
|
||||||
let instance = Instance::new(&InstanceDescriptor {
|
let mut instance = Instance::new(&InstanceDescriptor {
|
||||||
backends,
|
backends,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
// A build already pinned to GLES has nowhere to fall back to.
|
||||||
|
if backends != Backends::GL && instance.enumerate_adapters(backends).block_on().is_empty() {
|
||||||
|
log::warn!(
|
||||||
|
"iris renderer: no {backends:?} adapter on this device, falling back to GLES"
|
||||||
|
);
|
||||||
|
backends = Backends::GL;
|
||||||
|
instance = Instance::new(&InstanceDescriptor {
|
||||||
|
backends,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// SAFETY: the `NativeWindow` outlives the surface built from it --
|
// SAFETY: the `NativeWindow` outlives the surface built from it --
|
||||||
// android-view drops the old renderer (and this surface with it)
|
// android-view drops the old renderer (and this surface with it)
|
||||||
// before handing over a new window, in `surface_changed` below.
|
// before handing over a new window, in `surface_changed` below.
|
||||||
let surface = instance
|
let surface = instance
|
||||||
.create_surface(SurfaceTarget::from(AndroidWindowHandle { window }))
|
.create_surface(SurfaceTarget::from(AndroidWindowHandle { window }))
|
||||||
.expect("Could not create android surface!");
|
.map_err(|error| format!("Could not create the android surface: {error}"))?;
|
||||||
|
|
||||||
|
// Every step from here to a live device reports rather than
|
||||||
|
// panics, for the one reason: on the phone these builds run on
|
||||||
|
// there is no `adb`, so an abort's message reaches a tombstone
|
||||||
|
// nobody can read and the launcher simply restarts the app --
|
||||||
|
// which is what a crash loop with no explanation is. The caller
|
||||||
|
// (`android::view::IrisViewPeer::surface_changed`) puts this
|
||||||
|
// string on screen and in the app's own log ring instead.
|
||||||
let adapter = instance
|
let adapter = instance
|
||||||
.request_adapter(&RequestAdapterOptions {
|
.request_adapter(&RequestAdapterOptions {
|
||||||
power_preference: PowerPreference::default(),
|
power_preference: PowerPreference::default(),
|
||||||
@@ -137,19 +176,7 @@ impl AndroidRenderer {
|
|||||||
force_fallback_adapter: false,
|
force_fallback_adapter: false,
|
||||||
})
|
})
|
||||||
.block_on()
|
.block_on()
|
||||||
.expect("Could not get adapter!");
|
.map_err(|error| format!("No usable GPU adapter for backends {backends:?}: {error}"))?;
|
||||||
|
|
||||||
// Requesting the device itself still panics on failure: that is a
|
|
||||||
// `RequestDeviceError` (a limit or feature the adapter cannot grant
|
|
||||||
// at all), a different and already-diagnosable failure from the one
|
|
||||||
// this function now recovers from -- `RUST.md`'s "Software mode ...
|
|
||||||
// crashes for a third, different reason" is exactly that class, and
|
|
||||||
// its message already names the limit and the requested/allowed
|
|
||||||
// values with no truncation risk (it never reaches wgpu's
|
|
||||||
// uncaptured-error path). What this function's `Result` return
|
|
||||||
// covers is the *next* class of failure: the adapter grants the
|
|
||||||
// device, and validation only fails once a specific bind group
|
|
||||||
// layout is checked against it.
|
|
||||||
|
|
||||||
// Same request as the winit backend's `UiRenderer::new` -- no
|
// Same request as the winit backend's `UiRenderer::new` -- no
|
||||||
// binding-array features, see TEXTURES.md's "Recommended shape".
|
// binding-array features, see TEXTURES.md's "Recommended shape".
|
||||||
@@ -161,7 +188,13 @@ impl AndroidRenderer {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.block_on()
|
.block_on()
|
||||||
.expect("Could not get device!");
|
.map_err(|error| {
|
||||||
|
format!(
|
||||||
|
"The adapter {} ({:?}) refused a device: {error}",
|
||||||
|
adapter.get_info().name,
|
||||||
|
adapter.get_info().backend,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
// wgpu's default handler for an error raised outside `UiRenderNode::
|
// wgpu's default handler for an error raised outside `UiRenderNode::
|
||||||
// new`'s own error scopes (i.e. everything past device creation --
|
// new`'s own error scopes (i.e. everything past device creation --
|
||||||
|
|||||||
Reference in new issue
Block a user