Skip to main content

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    // Get current texture
350    let _get_current_texture = info_span!("acquire_texture").entered();
351    let render_texture = surface.get_current_texture();
352    drop(_get_current_texture);
353
354    // Check if texture is acquired
355    match render_texture {
356        Ok(surface_texture) => {
357            // Create render view
358            let render_view = surface_texture
359                .texture
360                .create_view(&wgpu::TextureViewDescriptor {
361                    label: Some("Main render texture"),
362                    format: match surface_config.format {
363                        wgpu::TextureFormat::Bgra8UnormSrgb => {
364                            Some(wgpu::TextureFormat::Bgra8UnormSrgb)
365                        }
366                        wgpu::TextureFormat::Rgba8UnormSrgb => {
367                            Some(wgpu::TextureFormat::Rgba8UnormSrgb)
368                        }
369                        wgpu::TextureFormat::Bgra8Unorm => Some(wgpu::TextureFormat::Bgra8Unorm),
370                        wgpu::TextureFormat::Rgba8Unorm => Some(wgpu::TextureFormat::Rgba8Unorm),
371                        _ => unreachable!(
372                            "Unsupported swapchain format: {:?}",
373                            surface_config.format
374                        )
375                    },
376                    dimension: Some(wgpu::TextureViewDimension::D2),
377                    aspect: wgpu::TextureAspect::All,
378                    base_mip_level: 0,
379                    mip_level_count: None,
380                    base_array_layer: 0,
381                    array_layer_count: None,
382                    usage: None
383                });
384            let cur_render = RenderTexture {
385                texture: surface_texture,
386                view: render_view
387            };
388            RenderEvent::Redraw(cur_render)
389        }
390        // Surface lost or outdated (minimized or moved to another screen)
391        Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => RenderEvent::Resize,
392        // System out of memory
393        Err(wgpu::SurfaceError::OutOfMemory) => {
394            error!("System out of memory.");
395            RenderEvent::None
396        }
397        // Timeout of the surface
398        Err(wgpu::SurfaceError::Timeout) => {
399            warn!("Timeout of the surface.");
400            RenderEvent::None
401        }
402        // Other errors
403        Err(e) => {
404            error!("Failed to acquire next swapchain texture: {:?}.", e);
405            RenderEvent::None
406        }
407    }
408}
409
410/// Present the rendered frame to the swapchain.
411///
412/// Call this after encoding commands that render into the acquired surface texture.
413pub fn present(surface_texture: SurfaceTexture) -> Result<(), RenderError> {
414    surface_texture.present();
415    Ok(())
416}
417
418/// Reconfigure a surface after a window resize or when the swapchain is lost.
419///
420/// The caller must also update `surface_config.width/height` before invoking.
421pub fn resize(device: &Device, surface: &Surface, surface_config: &SurfaceConfiguration) {
422    surface.configure(device, surface_config);
423}