wde_wgpu/
instance.rs

1//! Device/queue/surface bootstrap utilities used across the renderer.
2
3use bevy::{tasks::block_on, window::RawHandleWrapperHolder};
4use wde_logger::prelude::*;
5use wgpu::{
6    BackendOptions, Device, Features, Limits as WLimits, MemoryBudgetThresholds,
7    PresentMode as WPresentMode, Surface, SurfaceConfiguration, SurfaceTexture
8};
9
10use crate::texture::TextureView;
11
12pub type Limits = WLimits;
13pub type PresentMode = WPresentMode;
14
15/// Errors emitted by the renderer wrappers.
16///
17/// Match on this when guarding pipeline creation or pass submission:
18/// ```rust,no_run
19/// use wde_wgpu::instance::RenderError;
20///
21/// fn handle(error: RenderError) {
22///     match error {
23///         RenderError::MissingShader => eprintln!("shader missing"),
24///         RenderError::PipelineNotInitialized => eprintln!("call init() before drawing"),
25///         _ => eprintln!("other GPU error: {:?}", error),
26///     }
27/// }
28/// ```
29#[derive(Debug)]
30pub enum RenderError {
31    /// Cannot present render texture.
32    CannotPresent,
33    /// Cannot resize render instance.
34    CannotResize,
35    /// Pipeline not set.
36    PipelineNotSet,
37    /// Pipeline not initialized.
38    PipelineNotInitialized,
39    /// Missing a shader.
40    MissingShader,
41    /// Missing a vertex buffer.
42    MissingVertexBuffer,
43    /// Missing an index buffer.
44    MissingIndexBuffer,
45    /// Swapchain format not supported.
46    UnsupportedSwapchainFormat,
47    /// Depth format not supported.
48    UnsupportedDepthFormat,
49    /// Shader compilation error.
50    ShaderCompilationError,
51    /// Missing a bind group required by the shader.
52    MissingBindGroup,
53    /// Cannot create bind group layout.
54    CannotCreateBindGroupLayout,
55    /// Cannot create bind group.
56    CannotCreateBindGroup
57}
58
59/// Swapchain image plus an already-created view.
60#[derive(Debug)]
61pub struct RenderTexture {
62    /// Texture of the render texture.
63    pub texture: wgpu::SurfaceTexture,
64    /// View of the render texture.
65    pub view: TextureView
66}
67
68/// Results of attempting to acquire the next surface texture.
69///
70/// The [`RenderEvent::Resize`] variant signals that the swapchain must be reconfigured
71/// with [`resize`].
72#[derive(Debug)]
73pub enum RenderEvent {
74    /// Redraw the window.
75    Redraw(RenderTexture),
76    /// Resize the window.
77    Resize,
78    /// No event.
79    None
80}
81
82/// Device, queue, surface, and adapter bundle used to create GPU resources.
83///
84/// This is the handle you pass into buffers, textures, command buffers, and pipelines.
85/// The type is intentionally lightweight to clone/read behind the `Arc<RwLock<_>>` in
86/// `RenderInstance`.
87///
88/// ```rust,no_run
89/// use wde_wgpu::{buffer::{Buffer, BufferUsage}, instance::{create_instance, RenderInstanceData}};
90///
91/// let instance = create_instance("docs", Some(&window)).await;
92/// let data: std::sync::RwLockReadGuard<'_, RenderInstanceData> = instance.data.read().unwrap();
93///
94/// let mut buf = Buffer::new(&data, "hello", 16, BufferUsage::UNIFORM | BufferUsage::COPY_DST, None);
95/// buf.write(&data, &[1, 2, 3, 4], 0);
96/// ```
97pub struct RenderInstanceData<'a> {
98    /// Device of the instance.
99    pub device: Device,
100    /// Queue of the instance.
101    pub queue: wgpu::Queue,
102    /// Surface of the instance.
103    pub surface: Option<Surface<'a>>,
104    /// Adapter of the instance.
105    pub adapter: wgpu::Adapter,
106    /// Instance of the GPU device.
107    pub instance: wgpu::Instance,
108    /// Surface configuration of the instance.
109    pub surface_config: Option<SurfaceConfiguration>
110}
111
112/// Create a new GPU instance (device + queue) and optionally bind to a window surface.
113///
114/// `primary_window` must expose a native handle; on Bevy it is provided via
115/// `RawHandleWrapperHolder`.
116///
117/// # Example
118/// ```rust,no_run
119/// use bevy::window::RawHandleWrapperHolder;
120/// use wde_wgpu::instance::create_instance;
121///
122/// let render = create_instance("demo", Some(&window)).await;
123/// assert!(render.data.read().unwrap().surface.is_some());
124/// ```
125pub async fn create_instance(
126    label: &str,
127    primary_window: Option<&RawHandleWrapperHolder>
128) -> RenderInstanceData<'static> {
129    info!(label, "Creating render instance.");
130    let _trace = info_span!("wgpu-instance-create").entered();
131
132    // Set flags
133    let mut flags = if cfg!(debug_assertions) {
134        wgpu::InstanceFlags::DEBUG
135            | wgpu::InstanceFlags::VALIDATION
136            | wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL
137    } else {
138        wgpu::InstanceFlags::DISCARD_HAL_LABELS
139    };
140    if cfg!(feature = "gpu-debug") {
141        flags |= wgpu::InstanceFlags::GPU_BASED_VALIDATION
142    }
143
144    // Create wgpu instance
145    debug!(label, "Creating wgpu instance.");
146    let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
147        backends: wgpu::Backends::all(),
148        flags,
149        memory_budget_thresholds: MemoryBudgetThresholds::default(),
150        backend_options: BackendOptions::default()
151    });
152
153    // Retrieve surface
154    let surface = primary_window.and_then(|wrapper| unsafe {
155        let maybe_handle = wrapper
156            .0
157            .lock()
158            .expect("Couldn't get the window handle in time for renderer initialization");
159        if let Some(wrapper) = maybe_handle.as_ref() {
160            let handle = wrapper.get_handle();
161            Some(
162                instance
163                    .create_surface(handle)
164                    .expect("Failed to create wgpu surface")
165            )
166        } else {
167            None
168        }
169    });
170
171    // Retrieve adapter
172    let adapter = instance
173        .enumerate_adapters(wgpu::Backends::all())
174        .into_iter()
175        .find(|a| a.get_info().backend == wgpu::Backend::Vulkan)
176        .or_else(|| {
177            block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
178                power_preference: wgpu::PowerPreference::HighPerformance,
179                compatible_surface: surface.as_ref(),
180                ..Default::default()
181            }))
182            .ok()
183        })
184        .expect("Failed to find a suitable GPU adapter.");
185
186    // Check adaptater infos
187    let adapter_info = adapter.get_info();
188    info!(
189        "Using {} adapter {}.",
190        match adapter_info.device_type {
191            wgpu::DeviceType::DiscreteGpu => "discrete GPU",
192            wgpu::DeviceType::IntegratedGpu => "integrated GPU",
193            wgpu::DeviceType::Cpu => "CPU",
194            wgpu::DeviceType::VirtualGpu => "virtual GPU",
195            wgpu::DeviceType::Other => "other"
196        },
197        adapter_info.name
198    );
199    debug!("Adapter info: {:#?}", adapter_info);
200    if adapter_info.device_type == wgpu::DeviceType::Cpu {
201        warn!(
202            "The selected adapter is using a driver that only supports software rendering, this will be very slow."
203        );
204    }
205
206    // Set required features
207    let required_features =
208        wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES | Features::PUSH_CONSTANTS;
209
210    // Set limits
211    let required_limits = Limits {
212        max_push_constant_size: 128,
213        max_compute_invocations_per_workgroup: 1024,
214        ..Default::default()
215    };
216
217    // Create device instance and queue
218    trace!(label, "Requesting device.");
219    let (device, queue) = adapter
220        .request_device(&wgpu::DeviceDescriptor {
221            label: Some(label),
222            required_features,
223            required_limits,
224            memory_hints: wgpu::MemoryHints::Performance,
225            experimental_features: wgpu::ExperimentalFeatures::disabled(),
226            trace: wgpu::Trace::Off
227        })
228        .await
229        .unwrap_or_else(|e| panic!("Failed to create wgpu device: {:?}", e));
230
231    // Log device infos
232    debug!("Configured wgpu adapter limits: {:#?}", device.limits());
233    debug!("Configured wgpu adapter features: {:#?}", device.features());
234
235    // Handle uncaught device errors
236    device.on_uncaptured_error(std::sync::Arc::new(|error| match error {
237        wgpu::Error::OutOfMemory { source } => {
238            error!("Out of memory error from wgpu: {:#?}", source)
239        }
240        wgpu::Error::Validation {
241            description,
242            source
243        } => warn!(
244            "Uncaptured validation error from wgpu.\n{}\n{:#?}",
245            description, source
246        ),
247        wgpu::Error::Internal {
248            source,
249            description
250        } => error!("Internal error from wgpu: {}\n{:#?}", description, source)
251    }));
252
253    // Panic room
254    device.set_device_lost_callback(|reason, message| match reason {
255        wgpu::DeviceLostReason::Destroyed => error!("The GPU device was destroyed: {}", message),
256        wgpu::DeviceLostReason::Unknown => {
257            error!("The GPU device was lost for an unknown reason: {}", message)
258        }
259    });
260
261    // Return instance
262    RenderInstanceData {
263        device,
264        queue,
265        surface,
266        adapter,
267        instance,
268        surface_config: None
269    }
270}
271
272/// Configure a surface for presentation once the window size is known.
273///
274/// Call this once at startup and again whenever you handle a resize event.
275///
276/// # Example
277/// ```rust,no_run
278/// use wde_wgpu::instance::{setup_surface, PresentMode};
279///
280/// let config = setup_surface(label, size, device, surface, adapter, PresentMode::Fifo);
281/// println!("configured swapchain {}x{}", config.width, config.height);
282/// ```
283pub fn setup_surface(
284    label: &str,
285    size: (u32, u32),
286    device: &Device,
287    surface: &Surface,
288    adapter: &wgpu::Adapter,
289    present_mode: PresentMode
290) -> SurfaceConfiguration {
291    debug!(label, "Configuring surface.");
292
293    // Retrieve surface format (sRGB if possible)
294    let surface_caps = surface.get_capabilities(adapter);
295    debug!("Surface capabilities: {:#?}", surface_caps);
296    let surface_format = surface_caps
297        .formats
298        .iter()
299        .copied()
300        .find(|f| f.is_srgb())
301        .unwrap_or(surface_caps.formats[0]);
302
303    // Set texture usage
304    let mut usage = wgpu::TextureUsages::RENDER_ATTACHMENT;
305    if cfg!(debug_assertions) {
306        usage |= wgpu::TextureUsages::COPY_SRC;
307    }
308
309    // Set surface configuration
310    let surface_config = wgpu::SurfaceConfiguration {
311        usage,
312        format: surface_format,
313        width: size.0,
314        height: size.1,
315        present_mode,
316        alpha_mode: surface_caps.alpha_modes[0],
317        view_formats: vec![],
318        desired_maximum_frame_latency: 2
319    };
320    surface.configure(device, &surface_config);
321
322    surface_config
323}
324
325/// Acquire the next surface texture or signal that the surface needs reconfiguration.
326///
327/// Returns a [`RenderEvent::Redraw`] with a ready-to-use texture view when successful, or
328/// [`RenderEvent::Resize`] when the surface was lost/outdated. Forward errors like
329/// `OutOfMemory` as `RenderEvent::None` while logging.
330///
331/// # Example
332/// ```rust,no_run
333/// use wde_wgpu::instance::{get_current_texture, RenderEvent};
334///
335/// match get_current_texture(surface, config) {
336///     RenderEvent::Redraw(frame) => {
337///         // frame.texture -> call present after encoding work
338///         // frame.view -> attach to render passes
339///         drop(frame);
340///     }
341///     RenderEvent::Resize => println!("surface needs resize"),
342///     RenderEvent::None => println!("skipping frame"),
343/// }
344/// ```
345pub fn get_current_texture(
346    surface: &Surface,
347    surface_config: &SurfaceConfiguration
348) -> RenderEvent {
349    event!(LogLevel::TRACE, "Getting current texture.");
350
351    // Get current texture
352    let _get_current_texture = info_span!("acquire_texture").entered();
353    let render_texture = surface.get_current_texture();
354    drop(_get_current_texture);
355
356    // Check if texture is acquired
357    event!(
358        LogLevel::TRACE,
359        "Texture acquired. Creating render view and checking status."
360    );
361    match render_texture {
362        Ok(surface_texture) => {
363            // Create render view
364            let render_view = surface_texture
365                .texture
366                .create_view(&wgpu::TextureViewDescriptor {
367                    label: Some("Main render texture"),
368                    format: match surface_config.format {
369                        wgpu::TextureFormat::Bgra8UnormSrgb => {
370                            Some(wgpu::TextureFormat::Bgra8UnormSrgb)
371                        }
372                        wgpu::TextureFormat::Rgba8UnormSrgb => {
373                            Some(wgpu::TextureFormat::Rgba8UnormSrgb)
374                        }
375                        wgpu::TextureFormat::Bgra8Unorm => Some(wgpu::TextureFormat::Bgra8Unorm),
376                        wgpu::TextureFormat::Rgba8Unorm => Some(wgpu::TextureFormat::Rgba8Unorm),
377                        _ => unreachable!(
378                            "Unsupported swapchain format: {:?}",
379                            surface_config.format
380                        )
381                    },
382                    dimension: Some(wgpu::TextureViewDimension::D2),
383                    aspect: wgpu::TextureAspect::All,
384                    base_mip_level: 0,
385                    mip_level_count: None,
386                    base_array_layer: 0,
387                    array_layer_count: None,
388                    usage: None
389                });
390            let cur_render = RenderTexture {
391                texture: surface_texture,
392                view: render_view
393            };
394            RenderEvent::Redraw(cur_render)
395        }
396        // Surface lost or outdated (minimized or moved to another screen)
397        Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => RenderEvent::Resize,
398        // System out of memory
399        Err(wgpu::SurfaceError::OutOfMemory) => {
400            error!("System out of memory.");
401            RenderEvent::None
402        }
403        // Timeout of the surface
404        Err(wgpu::SurfaceError::Timeout) => {
405            warn!("Timeout of the surface.");
406            RenderEvent::None
407        }
408        // Other errors
409        Err(e) => {
410            error!("Failed to acquire next swapchain texture: {:?}.", e);
411            RenderEvent::None
412        }
413    }
414}
415
416/// Present the rendered frame to the swapchain.
417///
418/// Call this after encoding commands that render into the acquired surface texture.
419pub fn present(surface_texture: SurfaceTexture) -> Result<(), RenderError> {
420    event!(LogLevel::TRACE, "Presenting render texture.");
421    surface_texture.present();
422    Ok(())
423}
424
425/// Reconfigure a surface after a window resize or when the swapchain is lost.
426///
427/// The caller must also update `surface_config.width/height` before invoking.
428pub fn resize(device: &Device, surface: &Surface, surface_config: &SurfaceConfiguration) {
429    event!(
430        LogLevel::TRACE,
431        "Resizing surface to {}x{}.",
432        surface_config.width,
433        surface_config.height
434    );
435    surface.configure(device, surface_config);
436}