Skip to main content

loam_render/
device.rs

1use anyhow::Result;
2use std::fmt;
3use std::ops::Deref;
4use std::sync::{Arc, Mutex};
5use wgpu::*;
6
7pub const GPU_TIMER_FEATURES: Features =
8    Features::TIMESTAMP_QUERY.union(Features::TIMESTAMP_QUERY_INSIDE_ENCODERS);
9
10#[derive(Clone, Debug)]
11pub struct FeatureRequest {
12    pub required_features: Features,
13    pub optional_features: Features,
14    pub required_limits: Limits,
15}
16
17impl Default for FeatureRequest {
18    fn default() -> Self {
19        Self {
20            required_features: Features::empty(),
21            optional_features: GPU_TIMER_FEATURES,
22            required_limits: Limits::default(),
23        }
24    }
25}
26
27impl FeatureRequest {
28    pub fn resolve(
29        &self,
30        adapter_features: Features,
31        adapter_limits: &Limits,
32    ) -> std::result::Result<Features, MissingGpuCapability> {
33        let missing = self.required_features - adapter_features;
34        if !missing.is_empty() {
35            return Err(MissingGpuCapability::Feature(missing));
36        }
37        let mut missing_limit = None;
38        self.required_limits.check_limits_with_fail_fn(
39            adapter_limits,
40            true,
41            |name, required, available| {
42                missing_limit = Some(MissingGpuCapability::Limit {
43                    name,
44                    required,
45                    available,
46                });
47            },
48        );
49        match missing_limit {
50            Some(missing) => Err(missing),
51            None => Ok(self.required_features | (self.optional_features & adapter_features)),
52        }
53    }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum MissingGpuCapability {
58    Feature(Features),
59    Limit {
60        name: &'static str,
61        required: u64,
62        available: u64,
63    },
64}
65
66impl fmt::Display for MissingGpuCapability {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Feature(features) => write!(
70                f,
71                "this adapter does not provide the required GPU feature `{features:?}`"
72            ),
73            Self::Limit {
74                name,
75                required,
76                available,
77            } => write!(
78                f,
79                "this adapter limit `{name}` is {available}; the app requires {required}"
80            ),
81        }
82    }
83}
84
85impl std::error::Error for MissingGpuCapability {}
86
87#[cfg(test)]
88pub(crate) fn noop_context() -> GpuContext {
89    let instance = Instance::new(&InstanceDescriptor {
90        backends: Backends::NOOP,
91        backend_options: BackendOptions {
92            noop: NoopBackendOptions { enable: true },
93            ..Default::default()
94        },
95        ..Default::default()
96    });
97    pollster::block_on(GpuContext::new(instance, FeatureRequest::default(), None))
98        .expect("the noop backend always yields a context")
99}
100
101#[derive(Debug)]
102pub struct DeviceLoss {
103    pub reason: DeviceLostReason,
104    pub message: String,
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108enum GpuErrorKind {
109    OutOfMemory,
110    Validation,
111    Internal,
112}
113
114impl fmt::Display for GpuErrorKind {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::OutOfMemory => f.write_str("out of memory"),
118            Self::Validation => f.write_str("validation"),
119            Self::Internal => f.write_str("internal"),
120        }
121    }
122}
123
124#[derive(Debug)]
125pub struct UncapturedGpuError {
126    backend: Backend,
127    kind: GpuErrorKind,
128    cause: String,
129}
130
131impl fmt::Display for UncapturedGpuError {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(
134            f,
135            "{:?} backend {} error: {}",
136            self.backend, self.kind, self.cause
137        )
138    }
139}
140
141impl std::error::Error for UncapturedGpuError {}
142
143#[derive(Default)]
144struct DeviceSignals {
145    loss: Option<DeviceLoss>,
146    error: Option<UncapturedGpuError>,
147}
148
149#[derive(Default)]
150pub(crate) struct LossSignal(Mutex<DeviceSignals>);
151
152impl LossSignal {
153    fn take_loss(&self) -> Option<DeviceLoss> {
154        self.0
155            .lock()
156            .unwrap_or_else(|error| error.into_inner())
157            .loss
158            .take()
159    }
160
161    pub(crate) fn take_error(&self) -> Option<UncapturedGpuError> {
162        self.0
163            .lock()
164            .unwrap_or_else(|error| error.into_inner())
165            .error
166            .take()
167    }
168}
169
170pub struct GpuContext {
171    pub instance: Instance,
172    pub adapter: Adapter,
173    pub device: Device,
174    pub queue: Queue,
175    request: FeatureRequest,
176    loss: Arc<LossSignal>,
177}
178
179impl GpuContext {
180    pub async fn new(
181        instance: Instance,
182        request: FeatureRequest,
183        compatible_surface: Option<&Surface<'_>>,
184    ) -> Result<Self> {
185        let adapter = instance
186            .request_adapter(&RequestAdapterOptions {
187                compatible_surface,
188                power_preference: PowerPreference::HighPerformance,
189                force_fallback_adapter: false,
190            })
191            .await?;
192        let (device, queue, loss) = request_device(&adapter, &request).await?;
193        Ok(Self {
194            instance,
195            adapter,
196            device,
197            queue,
198            request,
199            loss,
200        })
201    }
202
203    pub fn take_device_loss(&self) -> Option<DeviceLoss> {
204        self.loss.take_loss()
205    }
206
207    pub fn take_uncaptured_error(&self) -> Option<UncapturedGpuError> {
208        self.loss.take_error()
209    }
210
211    pub(crate) fn loss_signal(&self) -> Arc<LossSignal> {
212        self.loss.clone()
213    }
214
215    /// A new device from the same adapter and request; the old device's work is cancelled and its late callbacks are ignored.
216    pub async fn recover(&mut self) -> Result<()> {
217        let (device, queue, loss) = request_device(&self.adapter, &self.request).await?;
218        self.device.destroy();
219        self.device = device;
220        self.queue = queue;
221        self.loss = loss;
222        Ok(())
223    }
224}
225
226async fn request_device(
227    adapter: &Adapter,
228    request: &FeatureRequest,
229) -> Result<(Device, Queue, Arc<LossSignal>)> {
230    let resolved = request.resolve(adapter.features(), &adapter.limits())?;
231    let (device, queue) = adapter
232        .request_device(&DeviceDescriptor {
233            label: Some("Loam Device"),
234            required_features: resolved,
235            required_limits: request.required_limits.clone(),
236            memory_hints: MemoryHints::default(),
237            trace: Trace::Off,
238            experimental_features: Default::default(),
239        })
240        .await?;
241    tracing::info!(
242        "GPU features enabled: {resolved:?}; optional features absent: {:?}",
243        request.optional_features - resolved
244    );
245    let backend = adapter.get_info().backend;
246    let loss = Arc::new(LossSignal::default());
247    let signal = loss.clone();
248    device.set_device_lost_callback(move |reason, message| {
249        let mut signal = signal.0.lock().unwrap_or_else(|error| error.into_inner());
250        signal.loss.get_or_insert(DeviceLoss { reason, message });
251    });
252    let signal = loss.clone();
253    device.on_uncaptured_error(Arc::new(move |error| {
254        let kind = match &error {
255            Error::OutOfMemory { .. } => GpuErrorKind::OutOfMemory,
256            Error::Validation { .. } => GpuErrorKind::Validation,
257            Error::Internal { .. } => GpuErrorKind::Internal,
258        };
259        let mut cause = error.to_string();
260        let mut source = std::error::Error::source(&error);
261        while let Some(next) = source {
262            cause.push_str(": ");
263            cause.push_str(&next.to_string());
264            source = next.source();
265        }
266        let mut signal = signal.0.lock().unwrap_or_else(|error| error.into_inner());
267        signal.error.get_or_insert(UncapturedGpuError {
268            backend,
269            kind,
270            cause,
271        });
272    }));
273    Ok((device, queue, loss))
274}
275
276pub struct OffscreenTarget {
277    #[allow(dead_code)]
278    texture: Texture,
279    pub view: TextureView,
280}
281
282#[derive(Clone, Copy, Debug, PartialEq, Eq)]
283struct PresentationSpec {
284    format: TextureFormat,
285    scene_format: Option<TextureFormat>,
286}
287
288struct Presentation {
289    spec: PresentationSpec,
290    scene_target: Option<OffscreenTarget>,
291    composite: Option<crate::composite::CompositeNode>,
292}
293
294impl Presentation {
295    fn build(device: &Device, spec: PresentationSpec, size: (u32, u32)) -> Self {
296        let composite = spec
297            .scene_format
298            .map(|_| crate::composite::CompositeNode::new(device, spec.format));
299        let mut presentation = Self {
300            spec,
301            scene_target: None,
302            composite,
303        };
304        presentation.resize(device, size);
305        presentation
306    }
307
308    fn resize(&mut self, device: &Device, size: (u32, u32)) {
309        let spec = self.spec;
310        if let (Some(scene_fmt), Some(composite)) = (spec.scene_format, self.composite.as_mut()) {
311            let scene = create_scene_target(device, scene_fmt, size.0, size.1);
312            composite.set_scene_view(device, &scene.view);
313            self.scene_target = Some(scene);
314        }
315    }
316
317    fn rebuild(&mut self, device: &Device, size: (u32, u32)) {
318        *self = Self::build(device, self.spec, size);
319    }
320}
321
322pub struct RenderDevice {
323    pub context: GpuContext,
324    presentation: Presentation,
325    size: (u32, u32),
326}
327
328impl Deref for RenderDevice {
329    type Target = GpuContext;
330
331    fn deref(&self) -> &GpuContext {
332        &self.context
333    }
334}
335
336impl RenderDevice {
337    pub fn new(context: GpuContext, surface_format: TextureFormat, size: (u32, u32)) -> Self {
338        let needs_composite = !surface_format.is_srgb();
339        let scene_format = needs_composite.then(|| surface_format.add_srgb_suffix());
340        if let Some(scene_fmt) = scene_format {
341            tracing::info!(
342                "non-sRGB surface; rendering through offscreen scene target {scene_fmt:?} \
343                 with composite pass to {surface_format:?} swapchain"
344            );
345        }
346        let presentation = Presentation::build(
347            &context.device,
348            PresentationSpec {
349                format: surface_format,
350                scene_format,
351            },
352            size,
353        );
354
355        Self {
356            context,
357            presentation,
358            size,
359        }
360    }
361
362    pub async fn recover(&mut self) -> Result<()> {
363        self.context.recover().await?;
364        self.presentation.rebuild(&self.context.device, self.size);
365        Ok(())
366    }
367
368    pub fn resize(&mut self, new_size: (u32, u32)) {
369        if new_size.0 == 0 || new_size.1 == 0 {
370            return;
371        }
372        self.size = new_size;
373        self.presentation.resize(&self.context.device, new_size);
374    }
375
376    /// Scene-pass target priority: this, then the swapchain view.
377    pub fn scene_view(&self) -> Option<&TextureView> {
378        self.presentation.scene_target.as_ref().map(|t| &t.view)
379    }
380
381    /// Pipeline constructors take this, not the surface format.
382    pub fn target_format(&self) -> TextureFormat {
383        self.presentation
384            .spec
385            .scene_format
386            .unwrap_or(self.presentation.spec.format)
387    }
388
389    pub fn composite_to_swap(&self, encoder: &mut wgpu::CommandEncoder, swap_view: &TextureView) {
390        if let Some(composite) = self.presentation.composite.as_ref() {
391            composite.run(encoder, swap_view);
392        }
393    }
394}
395
396fn create_scene_target(
397    device: &Device,
398    format: TextureFormat,
399    width: u32,
400    height: u32,
401) -> OffscreenTarget {
402    let texture = device.create_texture(&wgpu::TextureDescriptor {
403        label: Some("loam-render::scene_target"),
404        size: wgpu::Extent3d {
405            width,
406            height,
407            depth_or_array_layers: 1,
408        },
409        mip_level_count: 1,
410        sample_count: 1,
411        dimension: wgpu::TextureDimension::D2,
412        format,
413        usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
414        view_formats: &[],
415    });
416    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
417    OffscreenTarget { texture, view }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    const SIZE: (u32, u32) = (800, 600);
425
426    #[test]
427    fn launch_names_the_absent_required_feature_or_limit() {
428        let adapter_features = Features::TIMESTAMP_QUERY | Features::SHADER_F16;
429        let adapter_limits = Limits::default();
430        let request = FeatureRequest {
431            required_features: Features::SHADER_F16 | Features::MULTIVIEW,
432            optional_features: Features::TIMESTAMP_QUERY | Features::POLYGON_MODE_LINE,
433            required_limits: Limits::default(),
434        };
435        let error = request
436            .resolve(adapter_features, &adapter_limits)
437            .unwrap_err();
438        assert_eq!(error, MissingGpuCapability::Feature(Features::MULTIVIEW));
439        assert!(error.to_string().contains("MULTIVIEW"), "{error}");
440
441        let request = FeatureRequest {
442            required_features: Features::SHADER_F16,
443            required_limits: Limits {
444                max_texture_dimension_2d: adapter_limits.max_texture_dimension_2d * 2,
445                ..Limits::default()
446            },
447            ..request
448        };
449        let error = request
450            .resolve(adapter_features, &adapter_limits)
451            .unwrap_err();
452        assert_eq!(
453            error,
454            MissingGpuCapability::Limit {
455                name: "max_texture_dimension_2d",
456                required: u64::from(adapter_limits.max_texture_dimension_2d) * 2,
457                available: u64::from(adapter_limits.max_texture_dimension_2d),
458            }
459        );
460        assert!(
461            error.to_string().contains("max_texture_dimension_2d"),
462            "{error}"
463        );
464
465        let request = FeatureRequest {
466            required_limits: Limits::default(),
467            ..request
468        };
469        assert_eq!(
470            request.resolve(adapter_features, &adapter_limits),
471            Ok(Features::SHADER_F16 | Features::TIMESTAMP_QUERY)
472        );
473    }
474
475    fn render_target(device: &Device, format: TextureFormat) -> TextureView {
476        device
477            .create_texture(&TextureDescriptor {
478                label: Some("recovery target"),
479                size: Extent3d {
480                    width: SIZE.0,
481                    height: SIZE.1,
482                    depth_or_array_layers: 1,
483                },
484                mip_level_count: 1,
485                sample_count: 1,
486                dimension: TextureDimension::D2,
487                format,
488                usage: TextureUsages::RENDER_ATTACHMENT,
489                view_formats: &[],
490            })
491            .create_view(&TextureViewDescriptor::default())
492    }
493
494    #[test]
495    fn regenerable_presentation_resources_rebuild_on_the_new_device_after_device_loss() {
496        let mut context = noop_context();
497        let spec = PresentationSpec {
498            format: TextureFormat::Bgra8Unorm,
499            scene_format: Some(TextureFormat::Bgra8UnormSrgb),
500        };
501        let mut presentation = Presentation::build(&context.device, spec, SIZE);
502        let old_device = context.device.clone();
503        old_device.destroy();
504
505        pollster::block_on(context.recover()).unwrap();
506        presentation.rebuild(&context.device, SIZE);
507        let _ = old_device.poll(PollType::Poll);
508        assert!(
509            context.take_device_loss().is_none(),
510            "the old device's late loss callback reached the new context"
511        );
512
513        let device = &context.device;
514        device.push_error_scope(ErrorFilter::Validation);
515        let target = render_target(device, spec.format);
516        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
517        if let Some(composite) = presentation.composite.as_ref() {
518            composite.run(&mut encoder, &target);
519        }
520        context.queue.submit(Some(encoder.finish()));
521        let error = pollster::block_on(device.pop_error_scope());
522        assert!(
523            error.is_none(),
524            "a resource stayed on the lost device: {error:?}"
525        );
526
527        context.device.destroy();
528        let _ = context.device.poll(PollType::Poll);
529        let loss = context.take_device_loss().unwrap();
530        assert_eq!(loss.reason, DeviceLostReason::Destroyed);
531    }
532}