use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2}; use pollster::FutureExt; use std::sync::Arc; use std::time::Instant; use wgpu::*; use winit::{dpi::PhysicalSize, window::Window}; pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK; pub struct UiRenderer { window: Arc, surface: Surface<'static>, device: Device, queue: Queue, config: SurfaceConfiguration, view_format: TextureFormat, encoder: CommandEncoder, pub ui: UiRenderNode, } impl UiRenderer { pub fn update(&mut self, ui: &mut Ui) { self.ui.update(&self.device, &self.queue, ui); } /// The two waits, so a desktop frame divides up the same way an /// Android one does -- see `AndroidRenderer::draw` for why the /// swapchain acquire is measured apart from the work. 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. Named rather than swallowed: a window that stops // presenting silently is the state this file's `pre_present_notify` // comment was written about. 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.window.pre_present_notify(); self.queue.present(output); FrameParts::waits(acquire, submit_start.elapsed()) } pub fn resize(&mut self, size: &PhysicalSize) { self.config.width = size.width; self.config.height = size.height; self.surface.configure(&self.device, &self.config); self.ui.resize( Vec2::new(size.width as f32, size.height as f32), &self.queue, ); } fn create_encoder(device: &Device) -> CommandEncoder { device.create_command_encoder(&CommandEncoderDescriptor { label: Some("Render Encoder"), }) } pub fn new(window: Arc) -> Self { let size = window.inner_size(); // `force-gles` on the desktop too, not just on Android: the // GLES backend has behaviour of its own (a one-layer array // texture is a `GL_TEXTURE_2D` -- see // `GpuTextures::create_array_texture`), and a machine with a // real GPU is where that is cheap to reproduce and screenshot. let mut backends = if cfg!(feature = "force-gles") { Backends::GL } else { Backends::PRIMARY }; // The display handle comes from the window rather than being left // out: wgpu 30 asks for it whenever a GLES surface is going to be // presented on Wayland, which is exactly what the fallback below // produces on this machine. let mut instance = Instance::new(InstanceDescriptor { backends, ..InstanceDescriptor::new_with_display_handle(Box::new(window.clone())) }); if backends != Backends::GL && instance.enumerate_adapters(backends).block_on().is_empty() { log::warn!( "iris renderer: no {backends:?} adapter on this machine, falling back to GLES" ); backends = Backends::GL; instance = Instance::new(InstanceDescriptor { backends, ..InstanceDescriptor::new_with_display_handle(Box::new(window.clone())) }); } let surface = instance .create_surface(window.clone()) .expect("Could not create window surface!"); let adapter = instance .request_adapter(&RequestAdapterOptions { power_preference: PowerPreference::default(), compatible_surface: Some(&surface), force_fallback_adapter: false, ..Default::default() }) .block_on() .unwrap_or_else(|error| { panic!("No usable GPU adapter for backends {backends:?}: {error}") }); { let info = adapter.get_info(); log::info!( "iris renderer: {name} ({backend:?}, {driver}{driver_info}) on {backends:?}", name = info.name, backend = info.backend, driver = info.driver, driver_info = if info.driver_info.is_empty() { String::new() } else { format!(" {}", info.driver_info) }, ); } // No features beyond what wgpu asks for by default, and no // binding-array limits: the atlas is one texture_2d_array and a // standalone image is its own ordinary bind group, neither of which // needs descriptor indexing. See TEXTURES.md's "Recommended shape" // for why the old binding array asked for // VK_EXT_descriptor_indexing unconditionally and did not survive a // real share of Android GPUs. `iris_core::device_limits()` is // shared with the Android backend; see its own doc for why it is // not simply `Limits::default()`. let (device, queue) = adapter .request_device(&DeviceDescriptor { required_limits: iris_core::device_limits(), ..Default::default() }) .block_on() .expect("Could not get device!"); let surface_caps = surface.get_capabilities(&adapter); let formats = iris_core::srgb_surface_format(&surface_caps) .expect("Could not select an sRGB iris surface format"); 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: size.width, height: size.height, // Vsync, because a toolkit aiming at battery life must not present // frames a display will never show: AutoNoVsync accepts them as // fast as the GPU will take them, so a redraw burst costs whatever // the hardware can be made to do rather than one frame. // AutoVsync picks Fifo, which every backend supports. 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); // Unlike the Android backend, the desktop backend has no on-screen // fallback to show a diagnostic through, so a renderer-creation // failure still panics here -- but now with wgpu's full "Caused // by:" chain as the message, since `UiRenderNode::new` returns it // rather than letting wgpu's own default handler panic first (see // that function's doc comment). // Physical size, the same units the swapchain, `WindowEvent:: // Resized`, the pointer and the widget tree all use -- see // `desktop::content_scale` for why this backend stopped dividing // into a separate logical space, and what disagreed while it did. let physical_size = Vec2::new(size.width as f32, size.height as f32); let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size) .expect("Could not create iris render node!"); Self { surface, device, queue, config, view_format: formats.view, encoder, ui, window, } } pub fn window(&self) -> &Window { self.window.as_ref() } }