iris: the Android renderer falls back to GLES, and every failure reports

The bench app crash-looped on this checkout's emulator with the default
features (RUST.md's queue item). Not the surface lifecycle and not "once
backgrounded": a build without `force-gles` never got a first frame.
`AndroidRenderer::new` asked wgpu for `Backends::PRIMARY`, which does not
contain `GL`, and this emulator advertises a Vulkan ICD with no adapter
behind it -- `NotFound { active_backends: VULKAN, no_adapter_backends:
VULKAN, supported_backends: VULKAN | GL }`, `.expect`ed, so SIGABRT, so
the launcher restarts it. iris was refusing a device whose only usable
adapter is a GLES one.

It now probes for a `PRIMARY` adapter and rebuilds the instance on
`Backends::GL` when there is none. The probe runs on an instance that
never touches the window on purpose: **an Android window can be
connected to one graphics API only**, so one instance carrying both
backends fails worse -- measured here on the way to this fix, Vulkan's
`vkCreateAndroidSurfaceKHR` claims the window in `create_surface` and
the GLES surface from the same window then reports `In
Surface::configure / Invalid surface`, aborting a frame later in
`Surface::get_current_texture_view`. Vulkan still wins wherever it has
an adapter (`PowerPreference::None` does not sort, and Vulkan is
enumerated first), so nothing changes on the phone.

Second half, the same rule applied to the whole set: the surface,
adapter and device requests all report through the `Result<Self,
String>` this function already returns, where two of the three used to
panic. `surface_changed` puts that string on screen and in the log
ring, which is what the Result was added for.

Emulator evidence (API 36 x86_64, debug): 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. Clean on both the default and a `force-gles` build for the
cases this had no reason to touch: two background/return cycles,
rotation there and back (the `already_live=true` reuse branch), a
background/return after the rotation, and cold starts. Vulkan could not
be exercised here -- that this emulator has no Vulkan adapter is the
defect itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 21:50:02 -04:00
1 parent f99ae4c366
commit 85869d02f8
2 files changed
+81 -26

No files matched your search

+22
View File
@@ -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
+59 -26
View File
@@ -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 --