Skip to main content

rightkit_gpu/
context.rs

1//! The wgpu device context: instance, adapter pick, limits, device and queue, with the failure
2//! callbacks wired. Adapted from Ember `crates/gpu/src/ctx.rs` (the generic part; Ember's kernels,
3//! buffer pool and render paths stay in Ember).
4//!
5//! [`GpuContext::create`] enters the exit gate ([`crate::exit`]), writes the crash sentinel
6//! ([`crate::backend`]) around driver work, catches a panic, and refuses a software adapter and an
7//! adapter below the caller's storage-buffer minimum, each with a reason the app can show. An
8//! uncaptured wgpu error or a lost device is logged and reported once through the `on_failure`
9//! callback so the app can stop using the GPU.
10
11use std::sync::Arc;
12
13use crate::backend;
14use crate::exit;
15
16/// How the app wants the device made.
17#[derive(Debug, Clone)]
18pub struct ContextOptions {
19    /// The backends to load (see [`crate::backend::compute_backends`]).
20    pub backends: wgpu::Backends,
21    /// Prefer the adapter called this when present (the app's setting); else the default choice.
22    pub adapter_name: Option<String>,
23    /// Ask wgpu for the low-power adapter instead of the high-performance one.
24    pub low_power: bool,
25    /// The fewest storage buffers per shader stage the app's kernels bind; fewer is refused.
26    pub min_storage_buffers_per_stage: u32,
27    /// Lower the device limits, to reproduce a smaller adapter ([`parse_limits_cap`]).
28    pub limits_cap: Option<LimitsCap>,
29    /// The device's debug label.
30    pub label: String,
31}
32
33impl ContextOptions {
34    /// The default options for `backends`: default adapter, high performance, no limit cap.
35    pub fn new(backends: wgpu::Backends, label: impl Into<String>) -> Self {
36        Self {
37            backends,
38            adapter_name: None,
39            low_power: false,
40            min_storage_buffers_per_stage: 8,
41            limits_cap: None,
42            label: label.into(),
43        }
44    }
45}
46
47/// An upper bound on the device's limits.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct LimitsCap {
50    /// Most bytes in one storage binding.
51    pub storage_binding_bytes: u64,
52    /// Most bytes in one buffer.
53    pub buffer_bytes: u64,
54}
55
56/// Parses a limits cap the way Ember's `LIGHTCRAFT_GPU_LIMITS` does: `webgpu` / `downlevel` /
57/// `default` = the WebGPU defaults (128 MiB storage bindings, 256 MiB buffers), `<n>` (or `<n>MiB`,
58/// `<n>MB`) = n MiB storage bindings and 2n MiB buffers. Empty or unparsable: `None`.
59pub fn parse_limits_cap(text: &str) -> Option<LimitsCap> {
60    let v = text.trim().to_ascii_lowercase();
61    match v.as_str() {
62        "" => None,
63        "webgpu" | "downlevel" | "default" => {
64            let d = wgpu::Limits::default();
65            Some(LimitsCap {
66                storage_binding_bytes: d.max_storage_buffer_binding_size,
67                buffer_bytes: d.max_buffer_size,
68            })
69        }
70        n => {
71            let mb: u64 = n
72                .trim_end_matches("mib")
73                .trim_end_matches("mb")
74                .trim()
75                .parse()
76                .ok()?;
77            let bytes = mb.max(1) << 20;
78            Some(LimitsCap {
79                storage_binding_bytes: bytes,
80                buffer_bytes: 2 * bytes,
81            })
82        }
83    }
84}
85
86/// The index of the adapter called `wanted` in `names` (the first with that name), or `None` when
87/// there is none, so the default choice is used.
88pub fn pick_adapter(names: &[String], wanted: &str) -> Option<usize> {
89    names.iter().position(|n| n == wanted)
90}
91
92/// A wgpu device and queue on a chosen adapter.
93pub struct GpuContext {
94    pub device: wgpu::Device,
95    pub queue: wgpu::Queue,
96    pub info: wgpu::AdapterInfo,
97    /// The largest buffer the device allows (the smaller of the storage binding and buffer limits).
98    pub max_buffer: u64,
99}
100
101impl std::fmt::Debug for GpuContext {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("GpuContext")
104            .field("adapter", &self.info.name)
105            .field("backend", &self.info.backend)
106            .field("max_buffer", &self.max_buffer)
107            .finish()
108    }
109}
110
111/// The wgpu instance for `backends`: only these drivers are loaded.
112fn new_instance(backends: wgpu::Backends) -> wgpu::Instance {
113    let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
114    desc.backends = backends;
115    // DX12 shaders compile with FXC (issue #471).
116    desc.backend_options = backend::backend_options();
117    // No indirect-call validation pipeline: Intel Metal fails to compile it (#250).
118    desc.flags = backend::instance_flags_from_env(desc.flags);
119    wgpu::Instance::new(desc)
120}
121
122/// The adapters of `instance` that can render (software ones left out), in the order wgpu lists them.
123fn usable_adapters(instance: &wgpu::Instance, backends: wgpu::Backends) -> Vec<wgpu::Adapter> {
124    pollster::block_on(instance.enumerate_adapters(backends))
125        .into_iter()
126        .filter(|a| a.get_info().device_type != wgpu::DeviceType::Cpu)
127        .collect()
128}
129
130/// The names of the usable adapters for `backends`. Loads the drivers; the device is not created.
131pub fn adapter_names(backends: wgpu::Backends) -> Vec<String> {
132    usable_adapters(&new_instance(backends), backends)
133        .iter()
134        .map(|a| a.get_info().name)
135        .collect()
136}
137
138impl GpuContext {
139    /// Creates the device, or says why there is none (software adapter, too few storage buffers,
140    /// no adapter, device creation failed, the process is ending, or a driver panic).
141    ///
142    /// `on_failure` is called with a reason, possibly from a driver thread, when wgpu reports an
143    /// error outside any error scope or the device is lost; the app should stop using the GPU.
144    pub fn create(
145        options: &ContextOptions,
146        on_failure: impl Fn(String) + Send + Sync + 'static,
147    ) -> Result<GpuContext, String> {
148        // Device creation enters the driver and must not begin after process shutdown.
149        let Some(_work) = exit::enter() else {
150            return Err("the process is ending".into());
151        };
152        let on_failure: Arc<dyn Fn(String) + Send + Sync> = Arc::new(on_failure);
153        backend::with_init_marker(options.backends, || {
154            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
155                create_unguarded(options, &on_failure)
156            }))
157            .unwrap_or_else(|_| Err("device creation panicked".into()))
158        })
159    }
160}
161
162fn create_unguarded(
163    options: &ContextOptions,
164    on_failure: &Arc<dyn Fn(String) + Send + Sync>,
165) -> Result<GpuContext, String> {
166    let backends = options.backends;
167    let instance = new_instance(backends);
168    let power_preference = if options.low_power {
169        wgpu::PowerPreference::LowPower
170    } else {
171        wgpu::PowerPreference::HighPerformance
172    };
173    let by_name = options.adapter_name.as_deref().and_then(|wanted| {
174        let found = usable_adapters(&instance, backends);
175        let names: Vec<String> = found.iter().map(|a| a.get_info().name).collect();
176        let index = pick_adapter(&names, wanted);
177        if index.is_none() {
178            log::warn!(
179                "gpu: no adapter named {wanted:?} among {names:?}; using the default choice"
180            );
181        }
182        index.and_then(|i| found.into_iter().nth(i))
183    });
184    let adapter = match by_name {
185        Some(adapter) => adapter,
186        None => pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
187            power_preference,
188            ..Default::default()
189        }))
190        .map_err(|e| format!("no GPU adapter among {backends:?} ({e})"))?,
191    };
192    let info = adapter.get_info();
193    if info.device_type == wgpu::DeviceType::Cpu {
194        log::info!("gpu: only a software adapter ({})", info.name);
195        return Err(format!("software adapter ({}) skipped", info.name));
196    }
197    let mut limits = adapter.limits();
198    if limits.max_storage_buffers_per_shader_stage < options.min_storage_buffers_per_stage {
199        log::info!("gpu: {} has too few storage buffers per stage", info.name);
200        return Err(format!(
201            "{} has too few storage buffers per shader stage ({} < {})",
202            info.name,
203            limits.max_storage_buffers_per_shader_stage,
204            options.min_storage_buffers_per_stage
205        ));
206    }
207    if let Some(cap) = options.limits_cap {
208        limits.max_storage_buffer_binding_size = limits
209            .max_storage_buffer_binding_size
210            .min(cap.storage_binding_bytes);
211        limits.max_buffer_size = limits.max_buffer_size.min(cap.buffer_bytes);
212        log::info!(
213            "gpu: limits capped: storage bindings <= {} MiB, buffers <= {} MiB",
214            cap.storage_binding_bytes >> 20,
215            cap.buffer_bytes >> 20
216        );
217    }
218    let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
219        label: Some(options.label.as_str()),
220        required_limits: limits.clone(),
221        ..Default::default()
222    }))
223    .map_err(|e| format!("{}: device creation failed ({e})", info.name))?;
224
225    // Errors outside a caller's error scopes: log, and tell the app to stop using the GPU.
226    let uncaptured = Arc::clone(on_failure);
227    device.on_uncaptured_error(Arc::new(move |e| {
228        log::error!("gpu: {e}");
229        uncaptured(format!("device error: {e}"));
230    }));
231    // wgpu reports a lost device (driver reset, GPU hang, watchdog) only here, never as an error.
232    let lost = Arc::clone(on_failure);
233    device.set_device_lost_callback(move |reason, msg| {
234        log::error!("gpu: device lost ({reason:?}): {msg}");
235        lost(format!("device lost ({reason:?}): {msg}"));
236    });
237
238    let max_buffer = limits
239        .max_storage_buffer_binding_size
240        .min(limits.max_buffer_size);
241    log::info!(
242        "gpu: {} ({:?}), buffers <= {} MiB",
243        info.name,
244        info.backend,
245        max_buffer >> 20
246    );
247    Ok(GpuContext {
248        device,
249        queue,
250        info,
251        max_buffer,
252    })
253}