Skip to main content

limnus_wgpu_window/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/limnus
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use limnus_app::prelude::{App, Plugin};
6use limnus_default_stages::RenderFirst;
7use limnus_local_resource::prelude::LocalResource;
8use limnus_screen::WindowMessage;
9use limnus_system_params::{LoReM, Msg};
10use std::default::Default;
11use std::sync::Arc;
12use tracing::{debug, info, trace, warn};
13use wgpu::{
14    Adapter, BackendOptions, Backends, Device, DeviceDescriptor, Features, Instance,
15    InstanceDescriptor, InstanceFlags, Limits, MemoryBudgetThresholds, MemoryHints,
16    PowerPreference, Queue, RequestAdapterOptions, Surface, SurfaceConfiguration, SurfaceError,
17    Trace,
18};
19use winit::dpi::PhysicalSize;
20use winit::window::Window;
21
22#[derive(Debug, LocalResource)]
23pub struct WgpuWindow {
24    surface: Arc<Surface<'static>>,
25    device: Arc<Device>,
26    queue: Arc<Queue>,
27
28    config: SurfaceConfiguration,
29}
30
31impl WgpuWindow {
32    #[must_use]
33    pub const fn queue(&self) -> &Arc<Queue> {
34        &self.queue
35    }
36}
37
38pub struct ReceiveAnnoyingAsync {
39    pub device_info: Option<BasicDeviceInfo>,
40}
41
42#[derive(Debug, LocalResource)]
43pub struct BasicDeviceInfo {
44    pub adapter: Adapter,
45    pub device: Arc<Device>,
46    pub queue: Arc<Queue>,
47    pub surface: Arc<Surface<'static>>,
48    pub physical_surface_size: PhysicalSize<u32>,
49}
50
51async fn try_pref(
52    instance: &Instance,
53    surface: Option<&Surface<'_>>,
54    pref: PowerPreference,
55) -> Option<wgpu::Adapter> {
56    debug!(?pref, "trying to find adapter with power preference");
57    let res = instance
58        .request_adapter(&RequestAdapterOptions {
59            power_preference: pref,
60            compatible_surface: surface,
61            force_fallback_adapter: false,
62        })
63        .await;
64
65    match res {
66        Ok(adapter) => Some(adapter),
67        Err(err) => {
68            warn!("request_adapter({pref:?}) failed: {err}");
69            None
70        }
71    }
72}
73
74/// # Errors
75///
76pub async fn pick_best_adapter(
77    instance: &Instance,
78    surface: Option<&Surface<'_>>,
79) -> Result<Adapter, InitError> {
80    for &pref in &[
81        PowerPreference::HighPerformance,
82        PowerPreference::LowPower,
83        PowerPreference::None,
84    ] {
85        if let Some(a) = try_pref(instance, surface, pref).await {
86            return Ok(a);
87        }
88    }
89    Err(InitError::NoAdapter)
90}
91
92#[derive(Debug)]
93pub enum WgpuInitError {
94    NoAdapter,
95    NoDevice,
96}
97
98#[derive(Debug, Clone, Copy)]
99pub enum LimitsTier {
100    ModernDefault,
101    Downlevel,
102    WebGL2,
103}
104
105#[derive(Debug)]
106pub struct DeviceSelection {
107    pub device: Device,
108    pub queue: Queue,
109    pub tier: LimitsTier,
110    pub applied_limits: Limits,
111}
112
113async fn try_with(adapter: &Adapter, base: Limits, tier: LimitsTier) -> Option<DeviceSelection> {
114    // Use .using_resolution(adapter.limits()) before request_device to clamp your
115    // desired baseline limits to what the GPU actually supports. This avoids
116    // “requested limits exceed adapter capabilities” errors on older or downlevel GPUs.
117    let applied = base.using_resolution(adapter.limits());
118
119    info!(?tier, "trying to find device with tier");
120
121    adapter
122        .request_device(&DeviceDescriptor {
123            label: Some("wgpu device"),
124            required_features: Features::empty(), // require nothing
125            required_limits: applied.clone(),
126            experimental_features: Default::default(),
127            memory_hints: MemoryHints::default(),
128            trace: Trace::default(),
129        })
130        .await
131        .ok()
132        .map(|(device, queue)| DeviceSelection {
133            device,
134            queue,
135            tier,
136            applied_limits: applied,
137        })
138}
139
140/*
141    let device_descriptor = DeviceDescriptor {
142        label: None,
143        required_features: Features::empty(), // Specify features as needed
144        required_limits: if cfg!(target_arch = "wasm32") {
145            Limits::downlevel_webgl2_defaults() // TODO: Not sure if this is needed?
146        } else {
147            Limits::default()
148        },
149        memory_hints: MemoryHints::default(), // Use default memory hints
150        trace: Trace::default(),
151    };
152
153
154    debug!(?device_descriptor, "device descriptor");
155
156    let (device, queue) = adapter
157        .request_device(&device_descriptor)
158        .await
159        .expect("Failed to request device");
160    debug!(?device, "device");
161*/
162
163/// # Errors
164///
165pub async fn request_device_smart(adapter: &Adapter) -> Result<DeviceSelection, InitError> {
166    if let Some(sel) = try_with(adapter, Limits::default(), LimitsTier::ModernDefault).await {
167        return Ok(sel);
168    }
169    if let Some(sel) = try_with(adapter, Limits::downlevel_defaults(), LimitsTier::Downlevel).await
170    {
171        return Ok(sel);
172    }
173    if let Some(sel) = try_with(
174        adapter,
175        Limits::downlevel_webgl2_defaults(),
176        LimitsTier::WebGL2,
177    )
178    .await
179    {
180        return Ok(sel);
181    }
182
183    Err(InitError::NoDevice)
184}
185
186#[derive(Debug)]
187pub enum InitError {
188    CreateSurface(wgpu::CreateSurfaceError),
189    NoAdapter,
190    NoDevice,
191    RequestDevice(wgpu::RequestDeviceError),
192}
193
194/// # Errors
195///
196/// # Panics
197///
198pub async fn annoying_async_device_creation(
199    window: Arc<Window>,
200) -> Result<BasicDeviceInfo, InitError> {
201    let instance = Instance::new(&InstanceDescriptor {
202        backends: Backends::default(),
203        flags: InstanceFlags::default(),
204        memory_budget_thresholds: MemoryBudgetThresholds::default(),
205        backend_options: BackendOptions::default(),
206    });
207    debug!(?instance, "found instance");
208
209    let surface = instance
210        .create_surface(Arc::clone(&window))
211        .map_err(InitError::CreateSurface)?;
212    debug!(?surface, "found surface");
213
214    let adapter = pick_best_adapter(&instance, Some(&surface)).await?;
215    debug!(?adapter, "found adapter");
216
217    let device_selection = request_device_smart(&adapter).await?;
218    debug!(?device_selection, "found device selection");
219
220    let inner_size = window.inner_size();
221    debug!(?inner_size, "window size detected");
222
223    let device_info = BasicDeviceInfo {
224        adapter,
225        device: device_selection.device.into(),
226        queue: device_selection.queue.into(),
227        surface: surface.into(),
228        physical_surface_size: inner_size,
229    };
230
231    debug!(?device_info, "final device info selection");
232
233    Ok(device_info)
234}
235
236fn tick(mut wgpu_window: LoReM<WgpuWindow>, window_messages: Msg<WindowMessage>) {
237    for msg in window_messages.iter_previous() {
238        if let WindowMessage::Resized(size) = msg {
239            debug!("resized to {:?}", size);
240            wgpu_window.resize((size.x, size.y));
241        }
242    }
243}
244
245pub struct WgpuWindowPlugin;
246impl Plugin for WgpuWindowPlugin {
247    fn build(&self, _app: &mut App) {}
248
249    fn post_initialization(&self, app: &mut App) {
250        app.insert_local_resource(WgpuWindow::new(
251            app.local_resources().fetch::<BasicDeviceInfo>(),
252        ));
253        app.add_system(RenderFirst, tick);
254        info!("wgpu window plugin is done");
255    }
256}
257
258impl WgpuWindow {
259    #[must_use]
260    pub fn new(info: &BasicDeviceInfo) -> Self {
261        let config = Self::configure_render_surface(info);
262
263        Self {
264            device: Arc::clone(&info.device),
265            config,
266            queue: Arc::clone(&info.queue),
267            surface: Arc::clone(&info.surface),
268        }
269    }
270
271    #[must_use]
272    pub const fn device(&self) -> &Arc<Device> {
273        &self.device
274    }
275
276    fn configure_render_surface(info: &BasicDeviceInfo) -> SurfaceConfiguration {
277        let surface_caps = info.surface.get_capabilities(&info.adapter);
278        let surface_format = surface_caps
279            .formats
280            .iter()
281            .copied()
282            .find(wgpu::TextureFormat::is_srgb)
283            .unwrap_or(surface_caps.formats[0]);
284
285        let present_mode = wgpu::PresentMode::Fifo; // Fifo should be available on all devices
286        let config = wgpu::SurfaceConfiguration {
287            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
288            format: surface_format,
289            width: info.physical_surface_size.width,
290            height: info.physical_surface_size.height,
291            present_mode,
292            alpha_mode: surface_caps.alpha_modes[0],
293            desired_maximum_frame_latency: 2,
294            view_formats: vec![],
295        };
296
297        info.surface.configure(&info.device, &config);
298
299        let alpha_mode = surface_caps.alpha_modes[0];
300        trace!(
301            "found surface format {:?} {:?} {:?}",
302            surface_format, present_mode, alpha_mode
303        );
304
305        config
306    }
307
308    #[must_use]
309    pub const fn texture_format(&self) -> wgpu::TextureFormat {
310        self.config.format
311    }
312
313    pub fn configure_surface(&mut self) {
314        debug!("configured surface! {:?}", self.config);
315        self.surface.configure(&self.device, &self.config);
316    }
317
318    pub fn resize(&mut self, new_size: (u16, u16)) {
319        let width = new_size.0 as usize;
320        let height = new_size.1 as usize;
321
322        if width == 0 || height == 0 {
323            return;
324        }
325
326        self.config.width = width as u32;
327        self.config.height = height as u32;
328        self.configure_surface();
329    }
330
331    pub fn render(
332        &self,
333        mut render_fn: impl FnMut(&mut wgpu::CommandEncoder, &wgpu::TextureView),
334    ) -> Result<(), SurfaceError> {
335        let surface_texture = match self.surface.get_current_texture() {
336            Ok(frame) => frame,
337            Err(wgpu::SurfaceError::Outdated) => {
338                // Surface is out of date: reconfigure and bail out of this frame.
339                // TODO: Get window size somehow
340                //                self.surface.configure(&self.device, &self.config);
341                return Ok(()); // hopefully next frame will work
342            }
343            Err(wgpu::SurfaceError::Lost) => {
344                // Lost is treated the same as Outdated in wgpu 0.13+
345                return Ok(()); // hopefully next frame will work
346            }
347            Err(wgpu::SurfaceError::OutOfMemory) => {
348                // fatal – bail out of the app
349                eprintln!("GPU out of memory, exiting");
350                std::process::exit(1);
351            }
352            Err(e) => {
353                // other errors (Timeout, etc): log and skip this frame
354                eprintln!("wgpu error getting next swap chain texture: {e:?}");
355                return Err(SurfaceError::Other);
356            }
357        };
358        let texture_view = surface_texture
359            .texture
360            .create_view(&wgpu::TextureViewDescriptor::default());
361
362        let mut encoder = self
363            .device
364            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
365                label: Some("Render Encoder"),
366            });
367
368        render_fn(&mut encoder, &texture_view);
369
370        self.queue.submit(std::iter::once(encoder.finish()));
371
372        surface_texture.present();
373
374        Ok(())
375    }
376}