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 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 pub fn scene_view(&self) -> Option<&TextureView> {
378 self.presentation.scene_target.as_ref().map(|t| &t.view)
379 }
380
381 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}