Skip to main content

ravenrs_render/
gpu.rs

1use std::fmt;
2
3use ravenrs_core::{AdapterReport, GpuReport, SurfaceReport};
4
5/// A device and queue ready for rendering.
6#[derive(Debug, Clone)]
7pub struct GpuContext {
8    /// Physical GPU the device was created on.
9    pub adapter: wgpu::Adapter,
10    /// Logical device that owns all GPU resources.
11    pub device: wgpu::Device,
12    /// Command queue for uploads and submissions.
13    pub queue: wgpu::Queue,
14}
15
16/// Failure to set up the GPU for rendering.
17#[derive(Debug)]
18pub enum GpuError {
19    /// No adapter matched; on the web this usually means WebGPU is disabled.
20    NoAdapter(String),
21    /// The device request was rejected by the adapter.
22    Device(String),
23    /// The window or canvas surface could not be created or configured.
24    Surface(String),
25}
26
27impl fmt::Display for GpuError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::NoAdapter(e) => write!(f, "no compatible GPU adapter was found ({e})"),
31            Self::Device(e) => write!(f, "the GPU device could not be created ({e})"),
32            Self::Surface(e) => write!(f, "the drawing surface could not be created ({e})"),
33        }
34    }
35}
36
37impl std::error::Error for GpuError {}
38
39impl GpuContext {
40    /// Requests an adapter (compatible with `surface`, when given) and a device
41    /// with default limits, which every WebGPU implementation supports.
42    ///
43    /// A power preference is what makes wgpu rank adapters: without one it takes
44    /// the first it finds, which on Linux can be Vulkan on the CPU with the real
45    /// GPU right there on GL. An interface needs little, so it asks for the GPU
46    /// that spends the least; `WGPU_POWER_PREF` overrides that. When the adapter
47    /// cannot give the limits needed, as an old GPU may not, the others get a turn.
48    pub async fn new(
49        instance: &wgpu::Instance,
50        surface: Option<&wgpu::Surface<'_>>,
51    ) -> Result<Self, GpuError> {
52        let adapter = instance
53            .request_adapter(&wgpu::RequestAdapterOptions {
54                power_preference: wgpu::PowerPreference::from_env()
55                    .unwrap_or(wgpu::PowerPreference::LowPower),
56                force_fallback_adapter: false,
57                compatible_surface: surface,
58                ..Default::default()
59            })
60            .await
61            .map_err(|e| GpuError::NoAdapter(e.to_string()))?;
62        let (adapter, device, queue) = match request_device(&adapter).await {
63            Ok((device, queue)) => (adapter, device, queue),
64            Err(first) => {
65                tracing::warn!(adapter = %adapter.get_info().name, error = %first, "GPU refused; trying others");
66                let mut others = instance.enumerate_adapters(wgpu::Backends::all()).await;
67                others.retain(|other| {
68                    other.get_info() != adapter.get_info()
69                        && surface.is_none_or(|surface| other.is_surface_supported(surface))
70                });
71                others.sort_by_key(|other| other.get_info().device_type == wgpu::DeviceType::Cpu);
72                let mut found = None;
73                for other in others {
74                    if let Ok((device, queue)) = request_device(&other).await {
75                        found = Some((other, device, queue));
76                        break;
77                    }
78                }
79                found.ok_or(GpuError::Device(first))?
80            }
81        };
82        let info = adapter.get_info();
83        if info.device_type == wgpu::DeviceType::Cpu {
84            tracing::warn!(
85                adapter = %info.name,
86                driver = %info.driver,
87                "rendering on the CPU, so the interface will be slow; the GPU is not being used"
88            );
89        }
90        tracing::info!(adapter = ?info, "GPU ready");
91        Ok(Self { adapter, device, queue })
92    }
93}
94
95async fn request_device(adapter: &wgpu::Adapter) -> Result<(wgpu::Device, wgpu::Queue), String> {
96    let limits = if adapter.get_info().backend == wgpu::Backend::Gl && cfg!(target_arch = "wasm32") {
97        // WebGL2 offers less than WebGPU's defaults; the renderer needs no more than it has.
98        wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits())
99    } else if cfg!(target_arch = "wasm32") {
100        wgpu::Limits::default().using_resolution(adapter.limits())
101    } else {
102        wgpu::Limits::default()
103    };
104    adapter
105        .request_device(&wgpu::DeviceDescriptor {
106            label: Some("raven device"),
107            // Exact coverage adds up better in 32 bits, where the adapter can blend them.
108            // Timestamps stay unused until someone asks how long the GPU takes.
109            required_features: adapter.features()
110                & (wgpu::Features::FLOAT32_BLENDABLE | wgpu::Features::TIMESTAMP_QUERY),
111            required_limits: limits,
112            ..Default::default()
113        })
114        .await
115        .map_err(|e| e.to_string())
116}
117
118fn adapter_report(adapter: &wgpu::Adapter) -> AdapterReport {
119    let info = adapter.get_info();
120    AdapterReport {
121        name: info.name,
122        backend: info.backend.to_string(),
123        device_type: format!("{:?}", info.device_type),
124        driver: info.driver,
125        driver_info: info.driver_info,
126        software: info.device_type == wgpu::DeviceType::Cpu,
127    }
128}
129
130/// Describes `format` and `present_mode` on a surface with these capabilities.
131pub fn surface_report(
132    capabilities: &wgpu::SurfaceCapabilities,
133    format: wgpu::TextureFormat,
134    present_mode: wgpu::PresentMode,
135) -> SurfaceReport {
136    SurfaceReport {
137        format: format!("{format:?}"),
138        present_mode: format!("{present_mode:?}"),
139        supported_present_modes: capabilities.present_modes.iter().map(|m| format!("{m:?}")).collect(),
140    }
141}
142
143/// Describes the adapter in use and the alternatives. `permissive` is an instance
144/// that also accepts non-conformant drivers, since `instance` never shows those.
145pub async fn gpu_report(
146    active: &wgpu::Adapter,
147    instance: &wgpu::Instance,
148    permissive: &wgpu::Instance,
149    surface: Option<SurfaceReport>,
150) -> GpuReport {
151    let list = async |instance: &wgpu::Instance| -> Vec<AdapterReport> {
152        instance.enumerate_adapters(wgpu::Backends::all()).await.iter().map(adapter_report).collect()
153    };
154    let available = list(instance).await;
155    let hidden = list(permissive).await.into_iter().filter(|adapter| !available.contains(adapter)).collect();
156    GpuReport { active: adapter_report(active), available, hidden, surface }
157}
158
159/// Picks the surface format. Non-sRGB formats are preferred so blending happens
160/// in sRGB space, matching browsers and design tools; `*Srgb` formats still work
161/// because the shaders linearize colors when told to.
162pub fn preferred_surface_format(formats: &[wgpu::TextureFormat]) -> Option<wgpu::TextureFormat> {
163    formats.iter().copied().find(|f| !f.is_srgb()).or_else(|| formats.first().copied())
164}