use crate::task::RequestRedraw; use android_view::{ View, jni::{JavaVM, objects::GlobalRef}, ndk::native_window::NativeWindow, }; use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode}; use pollster::FutureExt; use std::time::Instant; use wgpu::{ rwh::{DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle}, *, }; pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK; /// `NativeWindow` (from the surface android-view hands over in /// `surfaceChanged`) has a window handle but not a display one -- there is /// exactly one display on Android and `rwh` has a unit variant for it. /// Mirrors android-view's own demo (`demo/src/lib.rs`'s /// `AndroidWindowHandle`). struct AndroidWindowHandle { window: NativeWindow, } impl HasDisplayHandle for AndroidWindowHandle { fn display_handle(&self) -> Result, HandleError> { Ok(DisplayHandle::android()) } } impl HasWindowHandle for AndroidWindowHandle { fn window_handle(&self) -> Result, HandleError> { self.window.window_handle() } } /// The android-view surface, unlike winit's window, does not outlive a /// backgrounding of the activity: `surfaceDestroyed`/`surfaceCreated` (via /// `SurfaceHolder.Callback`) recreate it, so this holds everything that /// depends on that surface rather than being built once at startup -- /// `AndroidUiState` holds it as `Option`, `None` exactly /// when there is no surface to draw into. pub struct AndroidRenderer { surface: Surface<'static>, device: Device, queue: Queue, config: SurfaceConfiguration, view_format: TextureFormat, encoder: CommandEncoder, pub ui: UiRenderNode, pub adapter_name: String, pub adapter_backend: Backend, pub adapter_driver: String, /// Every uncaptured wgpu error since this renderer was created -- see /// `iris_core::WgpuErrorLog`'s doc comment. Installed on `device` in /// `new()`, kept here so the Diagnostics page and the per-frame log in /// `update()` can both read it without a global. pub wgpu_errors: iris_core::WgpuErrorLog, frame_count: u64, /// Physical pixels per dp -- see `android::view::AndroidUiState:: /// content_scale`'s field comment for what this feeds. content_scale: f32, } /// One frame's worth of the counters `render/mod.rs`'s doc comments on /// `FrameUpdateStats`/`take_image_bind_group_creates`/ /// `take_atlas_pages_grown` describe -- assembled here because the three /// live on two different calling conventions (`FrameUpdateStats` from this /// exact `update()` call; the other two describe the *previous* frame, /// same as `bench_images`' existing use of them) and a diagnostic reader /// should not have to know that split. #[derive(Clone, Copy, Debug, Default)] pub struct FrameDiagnostics { pub masks_resized: bool, pub moves_resized: bool, pub paints_resized: bool, pub atlas_pages_grown_prev: u64, pub image_bind_group_creates_prev: u64, } impl AndroidRenderer { pub fn new( window: NativeWindow, width: u32, height: u32, content_scale: f32, ) -> Result { // 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 }; // No display handle: an Android surface is built from the // `NativeWindow` below, and there is no platform connection to hand // wgpu here the way there is on Wayland. let mut instance = Instance::new(InstanceDescriptor { backends, ..InstanceDescriptor::new_without_display_handle() }); // 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, ..InstanceDescriptor::new_without_display_handle() }); } // 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 })) .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(), compatible_surface: Some(&surface), force_fallback_adapter: false, ..Default::default() }) .block_on() .map_err(|error| format!("No usable GPU adapter for backends {backends:?}: {error}"))?; let (device, queue) = adapter .request_device(&DeviceDescriptor { required_limits: iris_core::device_limits(), ..Default::default() }) .block_on() .map_err(|error| { format!( "The adapter {} ({:?}) refused a device: {error}", adapter.get_info().name, adapter.get_info().backend, ) })?; let wgpu_errors = iris_core::WgpuErrorLog::default(); let wgpu_errors_for_handler = wgpu_errors.clone(); device.on_uncaptured_error(std::sync::Arc::new(move |error| { log::error!("iris wgpu uncaptured error: {error}"); wgpu_errors_for_handler.record(error); })); let info = adapter.get_info(); let adapter_name = info.name.clone(); let adapter_backend = info.backend; // Either half can be empty -- the emulator's GLES adapter reports // no `driver` and a long `driver_info`, so joining unconditionally // left a leading space in every log line it appears in. let adapter_driver = [info.driver.as_str(), info.driver_info.as_str()] .into_iter() .filter(|part| !part.is_empty()) .collect::>() .join(" "); // Say which adapter won, in the same words `desktop::render` uses, // and at startup rather than only on the Diagnostics page: the // backend alone (logged by `view.rs` when a renderer is built) does // not separate the cases that matter. In this checkout's emulator // `Gl` is the host's real GPU through virgl, and `Gl` under // `EMU_GPU=software` is SwiftShader on the CPU; on a phone `Vulkan` // is the device's own driver. A frame time or a screenshot with no // record of which of those produced it cannot be read, and the // fallback above is silent by design. log::info!( "iris renderer: {adapter_name} ({adapter_backend:?}, {adapter_driver}) on \ {backends:?}" ); let surface_caps = surface.get_capabilities(&adapter); let formats = iris_core::srgb_surface_format(&surface_caps)?; log::info!( "iris renderer: surface={:?}, view={:?}, color_space=Srgb", formats.surface, formats.view, ); let config = SurfaceConfiguration { usage: TextureUsages::RENDER_ATTACHMENT, format: formats.surface, // wgpu 30's new field; `Auto` is what every earlier version did. color_space: SurfaceColorSpace::Srgb, width, height, present_mode: PresentMode::AutoVsync, alpha_mode: surface_caps.alpha_modes[0], desired_maximum_frame_latency: 2, view_formats: (formats.view != formats.surface) .then_some(formats.view) .into_iter() .collect(), }; surface.configure(&device, &config); let encoder = Self::create_encoder(&device); let window_size = iris_core::util::Vec2::new(width as f32, height as f32); let ui = match UiRenderNode::new(&device, &queue, formats.view, window_size) { Ok(ui) => ui, Err(wgpu_error) => return Err(Self::diagnostic(&adapter, &wgpu_error)), }; Ok(Self { surface, device, queue, config, view_format: formats.view, encoder, ui, adapter_name, adapter_backend, adapter_driver, wgpu_errors, frame_count: 0, content_scale, }) } /// The adapter identity plus every limit and downlevel flag /// `create_bind_group_layout` validates a storage buffer or texture /// binding against, followed by wgpu's own error text -- everything a /// person reading this off a screenshot needs to tell "this adapter /// lacks X" from "this is a bug in the layout." Named explicitly rather /// than `{limits:?}`/`{flags:?}` wholesale, because `Limits` alone is /// dozens of fields nobody asked for -- these are exactly the ones /// `UiRenderNode::new`'s layouts (`rsc_layout`, `masks_layout`, /// `primitive_layout`) can fail against, per `CreateBindGroupLayoutError` /// (`wgpu-core::binding_model`) and its downlevel-flag checks /// (`wgpu-core::device::resource`, `VERTEX_STORAGE` in particular -- /// the one storage buffer here, `move_offsets`, that is visible to the /// vertex stage). fn diagnostic(adapter: &Adapter, wgpu_error: &str) -> String { let info = adapter.get_info(); let limits = adapter.limits(); let downlevel = adapter.get_downlevel_capabilities(); format!( "iris could not start rendering. Copy this text and send it to Iris.\n\n\ adapter: {name} ({backend:?}), driver: {driver} {driver_info}\n\ limits: max_storage_buffers_per_shader_stage={max_storage_buffers} \ max_sampled_textures_per_shader_stage={max_sampled_textures} \ max_bind_groups={max_bind_groups} \ max_bindings_per_bind_group={max_bindings} \ max_storage_buffer_binding_size={max_storage_binding} \ min_storage_buffer_offset_alignment={min_storage_align}\n\ downlevel flags: {flags:?}\n\n\ {wgpu_error}", name = info.name, backend = info.backend, driver = info.driver, driver_info = info.driver_info, max_storage_buffers = limits.max_storage_buffers_per_shader_stage, max_sampled_textures = limits.max_sampled_textures_per_shader_stage, max_bind_groups = limits.max_bind_groups, max_bindings = limits.max_bindings_per_bind_group, max_storage_binding = limits.max_storage_buffer_binding_size, min_storage_align = limits.min_storage_buffer_offset_alignment, flags = downlevel.flags, ) } pub fn diagnostics_report( &self, font: &iris_core::FontDiagnostics, frame_report: &str, ) -> String { let errors = self.wgpu_errors.snapshot(); let errors_text = if errors.is_empty() { "none".to_string() } else { errors.join("\n ") }; format!( "iris diagnostics. Copy this text and send it to Iris.\n\n\ adapter: {name} ({backend:?}), driver: {driver}\n\ surface: {surface:?}, view: {view:?}, color_space: Srgb\n\ content_scale: {content_scale}\n\ paint format: linear vec4\n\ atlas/image format: Rgba8UnormSrgb, views live: {views}\n\ fonts: {families_found} families found, default={default_family:?} \ mono={default_mono_family:?}\n\ fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \ mono={mono:?}\n\ icon font: {icons:?}\n\ wgpu errors since surface creation:\n {errors_text}\n\n\ {frame_report}", name = self.adapter_name, backend = self.adapter_backend, driver = self.adapter_driver, surface = self.config.format, view = self.view_format, content_scale = self.content_scale, views = self.ui.view_count(), families_found = font.families_found, default_family = font.default_family, default_mono_family = font.default_mono_family, regular = font.regular_resolved, bold = font.bold_resolved, italic = font.italic_resolved, mono = font.mono_resolved, icons = font.icon_family, ) } fn create_encoder(device: &Device) -> CommandEncoder { device.create_command_encoder(&CommandEncoderDescriptor { label: Some("Render Encoder"), }) } pub fn update(&mut self, ui: &mut Ui) -> FrameDiagnostics { let atlas_pages_grown_prev = self.ui.take_atlas_pages_grown(); let image_bind_group_creates_prev = self.ui.take_image_bind_group_creates(); let stats = self.ui.update(&self.device, &self.queue, ui); self.frame_count += 1; FrameDiagnostics { masks_resized: stats.masks_resized, moves_resized: stats.moves_resized, paints_resized: stats.paints_resized, atlas_pages_grown_prev, image_bind_group_creates_prev, } } pub fn frame_count(&self) -> u64 { self.frame_count } pub fn draw(&mut self) -> FrameParts { let acquire_start = Instant::now(); let output = match self.surface.get_current_texture() { CurrentSurfaceTexture::Success(texture) | CurrentSurfaceTexture::Suboptimal(texture) => texture, // wgpu 30 turned this Result into an enum; every arm here was an // `Err` the previous `.unwrap()` panicked on, except `Occluded`, // which is new. other => panic!("no surface texture to draw into: {other:?}"), }; let acquire = acquire_start.elapsed(); let view = output.texture.create_view(&TextureViewDescriptor { format: Some(self.view_format), ..Default::default() }); let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device)); { let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor { color_attachments: &[Some(RenderPassColorAttachment { view: &view, resolve_target: None, ops: Operations { load: LoadOp::Clear(CLEAR_COLOR.to_wgpu()), store: StoreOp::Store, }, depth_slice: None, })], ..Default::default() }); self.ui.draw(render_pass); } let submit_start = Instant::now(); self.queue.submit(std::iter::once(encoder.finish())); self.queue.present(output); FrameParts::waits(acquire, submit_start.elapsed()) } /// Physical pixels -- the unit layout and hit-testing use, matching /// the window uniform's own units. See /// `android::view::AndroidUiState::content_scale`'s field comment. pub fn size(&self) -> iris_core::util::Vec2 { iris_core::util::Vec2::new(self.config.width as f32, self.config.height as f32) } /// Reconfigures the surface and rewrites the window uniform for a new /// physical size -- deliberately the *only* two things this does. /// `device`, `ui`'s atlas, buffers and bind groups are untouched, so a /// call here (as opposed to a fresh `AndroidRenderer::new`) never /// invalidates a glyph the CPU-side cache already placed in the atlas. /// See `android::view::IrisViewPeer::surface_changed`'s doc comment for /// why that distinction matters -- it is what keeps text on screen /// across an IME resize. pub fn resize(&mut self, width: u32, height: u32) { self.config.width = width; self.config.height = height; self.surface.configure(&self.device, &self.config); let size = iris_core::util::Vec2::new(width as f32, height as f32); self.ui.resize(size, &self.queue); } } /// `Tasks`' redraw handle on Android: a background task finishes on the /// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so /// asking for a frame means attaching first. The global ref is what /// survives past the JNI call that handed the `View` to us. pub struct AndroidRedrawHandle { vm: JavaVM, view: GlobalRef, } impl AndroidRedrawHandle { pub fn new(vm: JavaVM, view: GlobalRef) -> Self { Self { vm, view } } } impl RequestRedraw for AndroidRedrawHandle { fn request_redraw(&self) { let Ok(mut env) = self.vm.attach_current_thread() else { return; }; let local = env.new_local_ref(&self.view).unwrap(); View(local).post_delayed(&mut env, 0); } }