//! Window surface rendering: swapchain configuration, resize, clear loop. use std::sync::Arc; use winit::window::Window; use super::{clear_view, Gpu, RenderError}; use crate::math::Color; use crate::window::RenderCtx; /// Renders to a window surface. /// /// Owns the [`Gpu`] plus the window's [`wgpu::Surface`] and its /// configuration. Stage 2 scope: every frame is cleared to /// [`clear_color`](Self::clear_color); draw passes come in later stages. pub struct RenderContext { gpu: Gpu, surface: wgpu::Surface<'static>, config: wgpu::SurfaceConfiguration, clear_color: Color, } impl RenderContext { /// Acquires the GPU and configures a surface for `window`. /// /// The window is held by `Arc` so the surface (which borrows it) can be /// `'static`, as winit hands out windows from its event loop. /// /// To run on any device, several render backends are tried in turn — the /// default (env-selected Vulkan/Metal/DX12), then GL, then a software /// adapter — and the first that produces a *configurable* surface wins. /// This is what lets the engine survive drivers that report a GPU but /// cannot present to the window's surface (e.g. old NVIDIA on Wayland under /// Vulkan, where `surface.configure` would otherwise fail). pub fn new(window: Arc) -> Result { // (label, backend override, force a software adapter) let attempts: [(&str, Option, bool); 3] = [ ("default", None, false), ("GL", Some(wgpu::Backends::GL), false), ("software", None, true), ]; let mut last_err: Option = None; for (i, &(label, backends, force_fallback)) in attempts.iter().enumerate() { match Self::try_backend(&window, backends, force_fallback) { Ok(ctx) => { if i > 0 { log::warn!("render backend fell back to '{label}'"); } return Ok(ctx); } Err(err) => { log::warn!("render backend '{label}' unavailable: {err}"); last_err = Some(err); } } } Err(last_err.unwrap_or(RenderError::NoWorkingBackend)) } /// Attempts one backend: build an instance (optionally forcing `backends`), /// create the surface, acquire an adapter/device (optionally a software /// one), and configure the surface. Any failure returns `Err` so the caller /// can try the next backend rather than aborting the process. fn try_backend( window: &Arc, backends: Option, force_fallback_adapter: bool, ) -> Result { let size = window.inner_size(); // The window doubles as the display handle (needed by GL/X11-style // backends); `from_env` keeps backend/flags overridable via WGPU_*. let mut desc = wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(window.clone())); if let Some(backends) = backends { desc.backends = backends; } let instance = wgpu::Instance::new(desc); let surface = instance.create_surface(window.clone())?; let gpu = Gpu::with_instance(instance, Some(&surface), force_fallback_adapter)?; let config = surface .get_default_config(gpu.adapter(), size.width.max(1), size.height.max(1)) .ok_or(RenderError::UnsupportedSurface)?; configure_surface(gpu.device(), &surface, &config)?; log::info!( "surface configured: {}x{} {:?} ({:?}) on {:?}", config.width, config.height, config.format, config.present_mode, gpu.adapter().get_info().backend, ); Ok(Self { gpu, surface, config, clear_color: Color::BLACK, }) } /// Reconfigures the surface for a new window size. Zero dimensions /// (minimized window) are clamped to 1 so the surface stays valid. pub fn resize(&mut self, width: u32, height: u32) { self.config.width = width.max(1); self.config.height = height.max(1); self.surface.configure(self.gpu.device(), &self.config); } /// Current surface size in physical pixels. pub fn size(&self) -> (u32, u32) { (self.config.width, self.config.height) } /// The surface's texture format. Apps need this to build render pipelines /// (or UI integrations) whose output matches the surface. pub fn surface_format(&self) -> wgpu::TextureFormat { self.config.format } /// The color the surface is cleared to each frame. pub fn clear_color(&self) -> Color { self.clear_color } /// Sets the clear color; takes effect on the next rendered frame. pub fn set_clear_color(&mut self, color: Color) { self.clear_color = color; } /// Renders one frame: acquires the next surface texture, clears it, and /// presents. Equivalent to [`render_frame_with`](Self::render_frame_with) /// with an empty draw hook. pub fn render_frame(&mut self, window: &Window) -> Result<(), RenderError> { self.render_frame_with(window, |_| {}) } /// Renders one frame, invoking `draw` after the clear and before present. /// /// The surface texture is acquired and cleared to /// [`clear_color`](Self::clear_color), then `draw` is handed a /// [`RenderCtx`] so it can record additional passes into the same view /// (use `LoadOp::Load` to preserve the clear), and finally the frame is /// presented. /// /// Lost or outdated surfaces (e.g. mid-resize) are reconfigured and the /// frame skipped; timed-out or occluded acquires skip the frame. All are /// normal transient conditions and not reported as errors. pub fn render_frame_with( &mut self, window: &Window, draw: impl FnOnce(&RenderCtx<'_>), ) -> Result<(), RenderError> { use wgpu::CurrentSurfaceTexture; let frame = match self.surface.get_current_texture() { // A suboptimal frame is still presentable; the next resize event // reconfigures the surface anyway. CurrentSurfaceTexture::Success(frame) | CurrentSurfaceTexture::Suboptimal(frame) => { frame } CurrentSurfaceTexture::Lost | CurrentSurfaceTexture::Outdated => { self.surface.configure(self.gpu.device(), &self.config); return Ok(()); } CurrentSurfaceTexture::Timeout | CurrentSurfaceTexture::Occluded => return Ok(()), CurrentSurfaceTexture::Validation => return Err(RenderError::SurfaceValidation), }; let view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); clear_view(self.gpu.device(), self.gpu.queue(), &view, self.clear_color); let ctx = RenderCtx { gpu: &self.gpu, view: &view, window, surface_format: self.config.format, size: (self.config.width, self.config.height), }; draw(&ctx); frame.present(); Ok(()) } /// The underlying GPU handle. pub fn gpu(&self) -> &Gpu { &self.gpu } } /// Configures `surface`, capturing any validation error instead of letting it /// reach wgpu's default (fatal, process-aborting) error handler. /// /// `surface.configure` returns `()` and reports failures through the device's /// error sink, which by default panics. Wrapping it in a validation error scope /// turns "Invalid surface" (and similar) into a recoverable [`Result`] so the /// caller can fall back to another backend. fn configure_surface( device: &wgpu::Device, surface: &wgpu::Surface<'static>, config: &wgpu::SurfaceConfiguration, ) -> Result<(), RenderError> { let scope = device.push_error_scope(wgpu::ErrorFilter::Validation); surface.configure(device, config); // `pop()` consumes the guard and yields any captured error. On native // backends the future is already resolved; `block_on` just unwraps it. if let Some(err) = pollster::block_on(scope.pop()) { return Err(RenderError::SurfaceConfigure(err.to_string())); } Ok(()) }