Skip to main content

henad_explore/
device.rs

1//! A GPU device acquired with no window or surface.
2//!
3//! Native only. `pollster` blocks on the request, and a browser cannot block.
4
5use std::fmt;
6
7use henad_compute::fault::FaultSink;
8use henad_compute::gpu::{GpuContext, GpuNeeds};
9use henad_compute::runtime_info::RuntimeInfo;
10
11/// Failure to acquire a headless device.
12#[derive(Debug)]
13pub enum DeviceError {
14    /// No adapter suits the request.
15    NoAdapter(wgpu::RequestAdapterError),
16    /// Adapter `adapter` offers less than the WebGPU baseline in limit `limit`, as a GL adapter sometimes does.
17    BelowBaseline {
18        /// Name of the adapter, as its driver reports it.
19        adapter: String,
20        /// Name of the first `wgpu::Limits` field the adapter falls short in, such as `max_buffer_size`.
21        limit: &'static str,
22    },
23    /// The adapter rejected the request to create a device.
24    NoDevice(wgpu::RequestDeviceError),
25}
26
27impl fmt::Display for DeviceError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::NoAdapter(_) => f.write_str("no suitable GPU adapter found"),
31            Self::BelowBaseline { adapter, limit } => {
32                write!(f, "adapter '{adapter}' offers less than the WebGPU baseline in {limit}")
33            }
34            Self::NoDevice(_) => f.write_str("failed to create GPU device"),
35        }
36    }
37}
38
39impl std::error::Error for DeviceError {
40    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
41        match self {
42            Self::NoAdapter(error) => Some(error),
43            Self::BelowBaseline { .. } => None,
44            Self::NoDevice(error) => Some(error),
45        }
46    }
47}
48
49/// Acquires a headless device for `needs`, with its adapter's [`RuntimeInfo`] attached to the context.
50///
51/// This is the device eframe acquires for henad-app, minus any window or surface. `henad-compute` never creates a
52/// device, so a non-GUI runner must create the device itself.
53///
54/// The device is requested at the WebGPU baseline on every backend, raised to `needs` by
55/// [`henad_compute::gpu::limits::raise`]. Note that an adapter below the baseline, as a GL adapter can be, gets no
56/// device here. henad-app uses a lower baseline for a GL adapter, draws with it and runs no GPU model on it.
57///
58/// # Errors
59///
60/// Returns [`DeviceError::NoAdapter`] when this machine offers no suitable adapter,
61/// [`DeviceError::BelowBaseline`] for an adapter below the baseline, and [`DeviceError::NoDevice`] when the adapter
62/// creates no device.
63pub fn acquire_headless(needs: GpuNeeds) -> Result<GpuContext, DeviceError> {
64    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
65    let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
66        power_preference: wgpu::PowerPreference::HighPerformance,
67        force_fallback_adapter: false,
68        compatible_surface: None,
69        ..Default::default()
70    }))
71    .map_err(DeviceError::NoAdapter)?;
72    let required_limits = device_limits(&adapter.limits(), |baseline| {
73        henad_compute::gpu::limits::raise(&adapter, baseline, needs)
74    })
75    .map_err(|limit| DeviceError::BelowBaseline {
76        adapter: adapter.get_info().name,
77        limit,
78    })?;
79    let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
80        label: Some("henad-explore"),
81        required_features: wgpu::Features::empty(),
82        required_limits,
83        memory_hints: wgpu::MemoryHints::Performance,
84        experimental_features: wgpu::ExperimentalFeatures::disabled(),
85        trace: wgpu::Trace::Off,
86    }))
87    .map_err(DeviceError::NoDevice)?;
88    let runtime = RuntimeInfo::collect(&adapter, &device);
89    // No surface exists, so `target_format` is arbitrary. The models' display texture is an offscreen `Rgba8Unorm`
90    // target, and a headless run never reads it back.
91    Ok(GpuContext::new(device, queue, wgpu::TextureFormat::Rgba8Unorm, FaultSink::new()).with_runtime_info(runtime))
92}
93
94/// Returns the limits to request from an adapter offering `available`, with `raise` applied to the WebGPU baseline.
95///
96/// The adapter is checked against the baseline before the raise. The raise clamps the storage buffer count to the
97/// adapter's count.
98///
99/// # Errors
100///
101/// Returns the name of the first baseline limit that `available` falls short of.
102pub(crate) fn device_limits(
103    available: &wgpu::Limits,
104    raise: impl FnOnce(&wgpu::Limits) -> wgpu::Limits,
105) -> Result<wgpu::Limits, &'static str> {
106    let baseline = wgpu::Limits::default();
107    match short_limit(&baseline, available) {
108        Some(limit) => Err(limit),
109        None => Ok(raise(&baseline)),
110    }
111}
112
113/// Returns the name of the first `required` limit that `available` falls short of, or `None` when it meets every
114/// limit.
115fn short_limit(required: &wgpu::Limits, available: &wgpu::Limits) -> Option<&'static str> {
116    let mut short = None;
117    required.check_limits_with_fail_fn(available, true, |name, _, _| short = Some(name));
118    short
119}
120
121#[cfg(test)]
122mod tests {
123    use super::{device_limits, short_limit};
124
125    #[test]
126    fn a_webgl2_adapter_falls_short_of_the_baseline() {
127        let baseline = wgpu::Limits::default();
128        assert_eq!(short_limit(&baseline, &baseline), None);
129        assert!(short_limit(&baseline, &wgpu::Limits::downlevel_webgl2_defaults()).is_some());
130    }
131
132    /// An adapter one storage buffer short of the baseline is rejected. The raise clamps the count to the adapter's 7,
133    /// and a check of the raised limits would pass it.
134    #[test]
135    fn an_adapter_short_of_storage_buffers_falls_short_of_the_baseline() {
136        let available = wgpu::Limits {
137            max_storage_buffers_per_shader_stage: 7,
138            ..wgpu::Limits::default()
139        };
140        let clamp = |base: &wgpu::Limits| wgpu::Limits {
141            max_storage_buffers_per_shader_stage: base
142                .max_storage_buffers_per_shader_stage
143                .min(available.max_storage_buffers_per_shader_stage),
144            ..base.clone()
145        };
146        assert_eq!(
147            device_limits(&available, clamp).err(),
148            Some("max_storage_buffers_per_shader_stage")
149        );
150        assert!(device_limits(&wgpu::Limits::default(), clamp).is_ok());
151    }
152}