diff --git a/docs/IRIS.md b/docs/IRIS.md index 4d53ee6..f634dee 100644 --- a/docs/IRIS.md +++ b/docs/IRIS.md @@ -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 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`, 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 A flick released at the wrong speed because the tracker averaged. It now diff --git a/iris/src/android/render.rs b/iris/src/android/render.rs index 3b04648..fcb1662 100644 --- a/iris/src/android/render.rs +++ b/iris/src/android/render.rs @@ -88,9 +88,10 @@ pub struct FrameDiagnostics { } impl AndroidRenderer { - /// `Err` holds a full, human-readable report -- wgpu's own error text - /// (`UiRenderNode::new`'s doc comment) plus the adapter identity and - /// the limits/downlevel flags bind-group-layout validation checks + /// `Err` holds a full, human-readable report for **every** way this + /// can fail -- no surface, no adapter, no device, or wgpu's own error + /// 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 /// 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 @@ -107,29 +108,67 @@ impl AndroidRenderer { height: u32, content_scale: f32, ) -> Result { - // `force-gles` (RUST.md's I5 "Where iris's frame time goes") swaps - // the software-Vulkan (SwiftShader) path for GLES/virgl on the same - // build, to isolate whether the backend itself explains the frame - // time gap against Compose. `cfg!` rather than a runtime switch: - // there is no way to hand an env var to an already-launched Android - // process on this machine (see the feature's doc in Cargo.toml). - let backends = if cfg!(feature = "force-gles") { + // `force-gles` (RUST.md's I5 "Where iris's frame time goes") pins + // the build to GLES, to isolate whether the backend itself explains + // the frame time gap against Compose. `cfg!` rather than a runtime + // switch: there is no way to hand an env var to an already-launched + // Android process on this machine (see the feature's doc in + // Cargo.toml). + // + // 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 } else { Backends::PRIMARY }; - let instance = Instance::new(&InstanceDescriptor { + let mut instance = Instance::new(&InstanceDescriptor { backends, ..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 -- // android-view drops the old renderer (and this surface with it) // before handing over a new window, in `surface_changed` below. let surface = instance .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 .request_adapter(&RequestAdapterOptions { power_preference: PowerPreference::default(), @@ -137,19 +176,7 @@ impl AndroidRenderer { force_fallback_adapter: false, }) .block_on() - .expect("Could not get adapter!"); - - // 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. + .map_err(|error| format!("No usable GPU adapter for backends {backends:?}: {error}"))?; // Same request as the winit backend's `UiRenderer::new` -- no // binding-array features, see TEXTURES.md's "Recommended shape". @@ -161,7 +188,13 @@ impl AndroidRenderer { ..Default::default() }) .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:: // new`'s own error scopes (i.e. everything past device creation --