oxiui_render_wgpu/gpu/device.rs
1//! Headless wgpu device acquisition and the offscreen colour target.
2//!
3//! [`GpuContext::headless`] performs the full no-window initialisation chain:
4//! `Instance` → `request_adapter` → `request_device`/`queue` → an offscreen
5//! colour texture. Every step that can fail returns a [`UiError`]; in
6//! particular, a missing adapter (no GPU available) yields
7//! [`UiError::Unsupported`] so callers — most importantly the headless tests —
8//! can *gracefully skip* rather than panic on machines without a usable GPU.
9//!
10//! The offscreen target uses [`wgpu::TextureFormat::Rgba8Unorm`] (the *linear*,
11//! non-sRGB variant) so that a solid colour written by the fragment shader is
12//! copied back byte-for-byte. An sRGB target would apply the OETF on store and
13//! distort the asserted pixel values.
14
15use oxiui_core::UiError;
16
17/// The non-sRGB offscreen colour format. Chosen so solid-colour readback is
18/// byte-exact (sRGB encoding would skew the stored values).
19pub const TARGET_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
20
21/// An initialised headless GPU context: device, queue, and an offscreen colour
22/// texture (plus its view) sized to the requested surface.
23///
24/// No window or swap-chain surface is involved — rendering goes to the
25/// offscreen texture, which can then be read back to CPU memory with
26/// [`crate::gpu::renderer`]'s readback path.
27pub struct GpuContext {
28 /// The logical GPU device.
29 pub device: wgpu::Device,
30 /// The command queue for the device.
31 pub queue: wgpu::Queue,
32 /// The offscreen colour texture (`RENDER_ATTACHMENT | COPY_SRC`).
33 pub color_texture: wgpu::Texture,
34 /// A default view over [`color_texture`](GpuContext::color_texture).
35 pub color_view: wgpu::TextureView,
36 /// Target width in physical pixels.
37 pub width: u32,
38 /// Target height in physical pixels.
39 pub height: u32,
40}
41
42impl GpuContext {
43 /// Initialise a headless GPU context with an offscreen target of
44 /// `width × height` pixels.
45 ///
46 /// # Errors
47 ///
48 /// * [`UiError::Unsupported`] — no GPU adapter is available (the caller
49 /// should treat this as "skip", not a hard failure), or the requested
50 /// target dimensions are zero.
51 /// * [`UiError::Backend`] — the adapter was found but the device request
52 /// failed.
53 pub fn headless(width: u32, height: u32) -> Result<Self, UiError> {
54 if width == 0 || height == 0 {
55 return Err(UiError::Unsupported(
56 "headless target dimensions must be non-zero".to_string(),
57 ));
58 }
59
60 // `Instance::default()` enables `Backends::all()` with no display handle
61 // — exactly what a headless (no-window) context needs. On this host
62 // that resolves to the Metal backend.
63 let instance = wgpu::Instance::default();
64
65 // Block on the async adapter request. A `None`/`Err` here means no GPU
66 // is usable on this host → surface as Unsupported so tests can skip.
67 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
68 power_preference: wgpu::PowerPreference::default(),
69 force_fallback_adapter: false,
70 compatible_surface: None,
71 }))
72 .map_err(|e| UiError::Unsupported(format!("no GPU adapter available: {e}")))?;
73
74 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
75 label: Some("oxiui-render-wgpu headless device"),
76 required_features: wgpu::Features::empty(),
77 required_limits: wgpu::Limits::downlevel_defaults(),
78 memory_hints: wgpu::MemoryHints::Performance,
79 experimental_features: wgpu::ExperimentalFeatures::disabled(),
80 trace: wgpu::Trace::Off,
81 }))
82 .map_err(|e| UiError::Backend(format!("GPU device request failed: {e}")))?;
83
84 let color_texture = device.create_texture(&wgpu::TextureDescriptor {
85 label: Some("oxiui-render-wgpu offscreen target"),
86 size: wgpu::Extent3d {
87 width,
88 height,
89 depth_or_array_layers: 1,
90 },
91 mip_level_count: 1,
92 sample_count: 1,
93 dimension: wgpu::TextureDimension::D2,
94 format: TARGET_FORMAT,
95 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
96 view_formats: &[],
97 });
98
99 let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
100
101 Ok(Self {
102 device,
103 queue,
104 color_texture,
105 color_view,
106 width,
107 height,
108 })
109 }
110}