iris: turn the phone bind-group-layout crash into a diagnostic, drop force-gles from phone builds
UiRenderNode::new used to let a wgpu validation error reach the default
uncaptured-error handler and panic, which is what aborted the P0 bench APK
on Iris's phone in AndroidRenderer::new with only "wgpu error: Validation
Error" surviving into the truncated crash report. It now wraps creation in
wgpu error scopes and returns Result<Self, String>; the Android backend
turns a failure into the adapter's identity, the limits/downlevel flags a
layout validates against, and wgpu's own error chain, logged as one logcat
line and shown on screen (IrisView.showRendererError) instead of crashing.
Auditing every bind-group-layout entry against wgpu-core's own validation
source names the likely cause: masks_layout's move_offsets storage buffer
is visible to the vertex stage, which Vulkan grants unconditionally but
GLES gates on the driver's own vertex-stage SSBO support -- and the
delivered APK was built with force-gles, a flag meant only to force the
*emulator* onto GLES for one frame-time measurement, that build-apk.sh's
default feature list applied to every arm64 build regardless of target.
Its default no longer includes force-gles.
Testing the diagnostic (by inducing an artificial validation error) also
found and fixed a real reentrancy bug: calling Activity.setContentView
synchronously from inside a ViewPeer callback re-enters the same peer's
RefCell borrow through onFocusChanged, aborting with "RefCell already
borrowed". Deferred through the same push_dynamic_deferred_callback
mechanism raise_if_enabled already uses.
Full audit, verification, and the named hypothesis are in RUST.md's P0
box ("iris bench crash on the phone, 2026-09-06"); the API change is in
IRIS.md. Nobody on this session has the phone, so this is unconfirmed
against real hardware -- the point of (1) is that the next run says so
either way.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
a27fbdb029
commit
46246ea511
11 files changed
+508
-13
No files matched your search
@@ -5,6 +5,12 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wgpu = { workspace = true }
|
||||
# Only for `UiRenderNode::new`'s `push_error_scope`/`pop_error_scope` pair
|
||||
# (renderer-creation error reporting, RUST.md's P0 phone-crash box) --
|
||||
# `block_on` turns that one async pop into the same synchronous call shape
|
||||
# `device_limits()`'s two callers already use for `request_adapter`/
|
||||
# `request_device`, rather than making this crate's one entry point async.
|
||||
pollster = { workspace = true }
|
||||
bytemuck ={ workspace = true }
|
||||
image = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use data::WindowUniform;
|
||||
use pollster::FutureExt;
|
||||
use wgpu::{
|
||||
util::{BufferInitDescriptor, DeviceExt},
|
||||
*,
|
||||
@@ -251,7 +252,34 @@ impl UiRenderNode {
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, queue: &Queue, config: &SurfaceConfiguration) -> Self {
|
||||
/// Builds every bind group layout, the pipeline, and the two storage
|
||||
/// buffers this needs -- fallibly, since this is exactly the call that
|
||||
/// aborted the process on Iris's phone in a release build with no
|
||||
/// message beyond "wgpu error: Validation Error" (RUST.md's P0 box,
|
||||
/// "iris bench crash on the phone, 2026-09-06"). wgpu's own default
|
||||
/// behaviour for an uncaptured error is `panic!` with no caller able to
|
||||
/// intervene, so every `create_bind_group_layout`/`create_render_pipeline`
|
||||
/// call below runs inside three nested error scopes (one per
|
||||
/// `ErrorFilter`) instead: whichever scope catches something, its
|
||||
/// `wgpu::Error`'s `Display` is wgpu-core's own `format_error` output
|
||||
/// (`"Validation Error\n\nCaused by:\n ..."`, the same text the panic
|
||||
/// would have printed before Android's crash reporter truncated it) and
|
||||
/// becomes this function's `Err`. Both callers
|
||||
/// (`android::render::AndroidRenderer::new`, `default::render::
|
||||
/// UiRenderer::new`) already call `Device`-creation with
|
||||
/// `pollster::block_on`, so returning a plain `Result` here rather than
|
||||
/// making this `async fn` keeps that same synchronous shape.
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
config: &SurfaceConfiguration,
|
||||
) -> Result<Self, String> {
|
||||
// Popped in reverse of this order, once every creation call below
|
||||
// has run -- `Device::push_error_scope`'s own contract.
|
||||
let oom_scope = device.push_error_scope(ErrorFilter::OutOfMemory);
|
||||
let validation_scope = device.push_error_scope(ErrorFilter::Validation);
|
||||
let internal_scope = device.push_error_scope(ErrorFilter::Internal);
|
||||
|
||||
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: Some("UI Shape Shader"),
|
||||
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
||||
@@ -373,7 +401,18 @@ impl UiRenderNode {
|
||||
cache: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
// Reverse of the push order above. Only one of these should ever be
|
||||
// `Some` in practice -- three separate scopes exist to name *which*
|
||||
// kind of error it was, not because more than one is expected at
|
||||
// once.
|
||||
let internal_err = internal_scope.pop().block_on();
|
||||
let validation_err = validation_scope.pop().block_on();
|
||||
let oom_err = oom_scope.pop().block_on();
|
||||
if let Some(err) = validation_err.or(oom_err).or(internal_err) {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
uniform_group,
|
||||
primitive_layout,
|
||||
rsc_layout,
|
||||
@@ -387,7 +426,7 @@ impl UiRenderNode {
|
||||
move_offsets,
|
||||
masks_layout,
|
||||
masks_group,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn bind_group_0(
|
||||
|
||||
Reference in new issue
Block a user