roxlap_gpu/headless.rs
1//! GPU.2 — headless device + queue for tests and offline tools.
2//!
3//! Stands up a wgpu device with no surface — the surface is the only
4//! part of `GpuRenderer` that needs a host window handle. Tests that
5//! exercise the
6//! decompressed-chunk upload + read-back path want neither a window
7//! nor a swapchain; this module exposes that bare device.
8//!
9//! Same `GpuInitError` surface as `GpuRenderer::new` so callers can
10//! share the fall-back-to-CPU pattern.
11
12use crate::{GpuInitError, GpuRendererSettings, PowerPreference};
13
14/// Bare wgpu device + queue with no surface. Suitable for
15/// `wgpu::ComputePass` work that doesn't present.
16pub struct HeadlessGpu {
17 /// The wgpu device — create buffers/pipelines against this exactly
18 /// as with the windowed renderer's device.
19 pub device: wgpu::Device,
20 /// The device's submission queue for uploads + compute dispatches.
21 pub queue: wgpu::Queue,
22 /// Human-readable adapter description
23 /// (`"name (backend, device_type)"`) for logs and test skips.
24 pub adapter_info: String,
25}
26
27impl HeadlessGpu {
28 /// # Errors
29 /// Returns [`GpuInitError::NoAdapter`] if no compatible adapter
30 /// is present (no Vulkan/Metal/DX12 driver), or
31 /// [`GpuInitError::RequestDevice`] if device creation fails.
32 pub async fn new(settings: GpuRendererSettings) -> Result<Self, GpuInitError> {
33 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
34 let power_preference = match settings.power_preference {
35 PowerPreference::Low => wgpu::PowerPreference::LowPower,
36 PowerPreference::High => wgpu::PowerPreference::HighPerformance,
37 };
38 let adapter = instance
39 .request_adapter(&wgpu::RequestAdapterOptions {
40 power_preference,
41 compatible_surface: None,
42 force_fallback_adapter: false,
43 })
44 .await
45 .map_err(|_| GpuInitError::NoAdapter)?;
46 let info = adapter.get_info();
47 let adapter_info = format!(
48 "{name} ({backend:?}, {device_type:?})",
49 name = info.name,
50 backend = info.backend,
51 device_type = info.device_type,
52 );
53 let (device, queue) = adapter
54 .request_device(&wgpu::DeviceDescriptor {
55 label: Some("roxlap-gpu headless device"),
56 required_features: wgpu::Features::empty(),
57 required_limits: crate::pick_required_limits(&adapter.limits()),
58 experimental_features: wgpu::ExperimentalFeatures::disabled(),
59 memory_hints: wgpu::MemoryHints::default(),
60 trace: wgpu::Trace::Off,
61 })
62 .await?;
63 Ok(Self {
64 device,
65 queue,
66 adapter_info,
67 })
68 }
69
70 /// Synchronous wrapper for tests / hosts without an async runtime.
71 ///
72 /// # Errors
73 /// See [`Self::new`].
74 pub fn new_blocking(settings: GpuRendererSettings) -> Result<Self, GpuInitError> {
75 pollster::block_on(Self::new(settings))
76 }
77}