Skip to main content

trueno_graph/gpu/
device.rs

1//! GPU device initialization and management
2//!
3//! Handles wgpu device creation, adapter selection, and GPU resource lifecycle.
4
5use thiserror::Error;
6use wgpu::util::DeviceExt;
7
8/// Platform-appropriate wgpu backend mask for adapter enumeration.
9///
10/// PMAT-927 (class follow-up to PMAT-925): `wgpu::Backends::all()` includes
11/// [`wgpu::Backends::GL`], which on Linux hosts that expose both Vulkan and
12/// GLES/EGL (notably the intel AMD-RADV cross-silicon baseline box) instantiates
13/// a GLES adapter whose `EglContext::make_current` **panics inside `Drop`**. A
14/// panic in a destructor during cleanup aborts the whole process with SIGABRT
15/// ("panic in a destructor during cleanup"); standalone enumeration can also
16/// spin/hang on the broken EGL path. The graph compute kernels themselves are
17/// correct — the fragility is purely in adapter *enumeration* and the GLES
18/// `Drop` path.
19///
20/// We return [`wgpu::Backends::PRIMARY`], which in wgpu 22 (this crate's pin) is
21/// `VULKAN | METAL | DX12 | BROWSER_WEBGPU` and **excludes** `GL` (GL lives only
22/// in `Backends::SECONDARY`). This keeps the real GPU on every platform — Vulkan
23/// on Linux/AMD-RADV, Metal on Apple, DX12 on Windows — while guaranteeing the
24/// broken GLES/EGL adapter is never created.
25#[must_use]
26pub const fn gpu_backends() -> wgpu::Backends {
27    // PRIMARY = VULKAN | METAL | DX12 | BROWSER_WEBGPU (never GL/GLES).
28    wgpu::Backends::PRIMARY
29}
30
31/// GPU device initialization errors
32#[derive(Debug, Error)]
33pub enum GpuDeviceError {
34    /// No compatible GPU adapter found
35    #[error("No compatible GPU adapter found")]
36    NoAdapter,
37
38    /// Failed to request GPU device
39    #[error("Failed to request GPU device: {0}")]
40    DeviceRequest(String),
41
42    /// GPU feature not supported
43    #[error("GPU feature not supported: {0}")]
44    UnsupportedFeature(String),
45}
46
47/// GPU device wrapper for graph operations
48///
49/// # Example
50///
51/// ```ignore
52/// # use trueno_graph::gpu::GpuDevice;
53/// let device = GpuDevice::new().await?;
54/// assert!(device.is_available());
55/// ```
56#[derive(Debug)]
57pub struct GpuDevice {
58    #[allow(dead_code)]
59    device: wgpu::Device,
60    #[allow(dead_code)]
61    queue: wgpu::Queue,
62    #[allow(dead_code)]
63    adapter: wgpu::Adapter,
64}
65
66impl GpuDevice {
67    /// Check if GPU is available without creating a device
68    ///
69    /// This is useful for tests to skip gracefully when GPU is not available.
70    pub async fn is_gpu_available() -> bool {
71        Self::new().await.is_ok()
72    }
73
74    /// Initialize GPU device with default settings
75    ///
76    /// # Errors
77    ///
78    /// Returns `GpuDeviceError` if:
79    /// - No compatible GPU adapter found
80    /// - Device request fails
81    /// - Required features not supported
82    pub async fn new() -> Result<Self, GpuDeviceError> {
83        // PMAT-927: use the non-GLES mask (see `gpu_backends`) instead of
84        // Backends::all() so the broken GLES/EGL adapter (SIGABRT-in-Drop on
85        // Linux/AMD-RADV) is never registered.
86        Self::new_with_backend(gpu_backends()).await
87    }
88
89    /// Initialize GPU device with specific backend
90    ///
91    /// # Errors
92    ///
93    /// Returns `GpuDeviceError` if device initialization fails
94    pub async fn new_with_backend(backends: wgpu::Backends) -> Result<Self, GpuDeviceError> {
95        // Create wgpu instance
96        let instance =
97            wgpu::Instance::new(wgpu::InstanceDescriptor { backends, ..Default::default() });
98
99        // Request adapter (GPU)
100        let adapter = instance
101            .request_adapter(&wgpu::RequestAdapterOptions {
102                power_preference: wgpu::PowerPreference::HighPerformance,
103                compatible_surface: None,
104                force_fallback_adapter: false,
105            })
106            .await
107            .ok_or(GpuDeviceError::NoAdapter)?;
108
109        // Request device and queue
110        let (device, queue) = adapter
111            .request_device(
112                &wgpu::DeviceDescriptor {
113                    label: Some("trueno-graph GPU device"),
114                    required_features: wgpu::Features::empty(),
115                    required_limits: wgpu::Limits::default(),
116                    memory_hints: wgpu::MemoryHints::default(),
117                },
118                None,
119            )
120            .await
121            .map_err(|e| GpuDeviceError::DeviceRequest(e.to_string()))?;
122
123        Ok(Self { device, queue, adapter })
124    }
125
126    /// Check if GPU is available
127    #[must_use]
128    pub fn is_available(&self) -> bool {
129        true // If we constructed successfully, GPU is available
130    }
131
132    /// Get adapter info (GPU name, backend, etc.)
133    #[must_use]
134    pub fn info(&self) -> wgpu::AdapterInfo {
135        self.adapter.get_info()
136    }
137
138    /// Create GPU buffer with initial data
139    ///
140    /// # Errors
141    ///
142    /// Returns error if buffer creation fails (typically won't happen with wgpu)
143    pub fn create_buffer_init(
144        &self,
145        label: &str,
146        contents: &[u8],
147        usage: wgpu::BufferUsages,
148    ) -> Result<wgpu::Buffer, GpuDeviceError> {
149        Ok(self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
150            label: Some(label),
151            contents,
152            usage,
153        }))
154    }
155
156    /// Create empty GPU buffer
157    ///
158    /// # Errors
159    ///
160    /// Returns error if buffer creation fails
161    pub fn create_buffer(
162        &self,
163        label: &str,
164        size: u64,
165        usage: wgpu::BufferUsages,
166    ) -> Result<wgpu::Buffer, GpuDeviceError> {
167        Ok(self.device.create_buffer(&wgpu::BufferDescriptor {
168            label: Some(label),
169            size,
170            usage,
171            mapped_at_creation: false,
172        }))
173    }
174
175    /// Get device reference
176    #[must_use]
177    pub const fn device(&self) -> &wgpu::Device {
178        &self.device
179    }
180
181    /// Get queue reference
182    #[must_use]
183    pub const fn queue(&self) -> &wgpu::Queue {
184        &self.queue
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    /// PMAT-927 FALSIFIER: the wgpu adapter-enumeration backend mask used by
193    /// aprender-graph MUST NOT contain GLES (`wgpu::Backends::GL`), and MUST
194    /// contain the platform's real backend.
195    ///
196    /// RED on `Backends::all()` (contains GL → GLES/EGL adapter → SIGABRT-in-Drop
197    /// on Linux/AMD-RADV). GREEN on `Backends::PRIMARY`. Host-independent: it
198    /// inspects the bitmask, it does not create any adapter.
199    #[test]
200    fn test_gpu_backends_excludes_gles() {
201        let mask = gpu_backends();
202
203        // The whole point: GLES/EGL must never be enumerated.
204        assert!(
205            !mask.contains(wgpu::Backends::GL),
206            "gpu_backends() must NOT include Backends::GL (GLES/EGL panics in Drop \
207             on Linux/AMD-RADV → SIGABRT). mask = {mask:?}"
208        );
209
210        // The real GPU backend on each platform must still be present.
211        #[cfg(any(target_os = "linux", target_os = "android"))]
212        assert!(
213            mask.contains(wgpu::Backends::VULKAN),
214            "gpu_backends() must include VULKAN on Linux (AMD-RADV/NVIDIA). mask = {mask:?}"
215        );
216        #[cfg(target_os = "macos")]
217        assert!(
218            mask.contains(wgpu::Backends::METAL),
219            "gpu_backends() must include METAL on macOS (Apple Silicon). mask = {mask:?}"
220        );
221        #[cfg(target_os = "windows")]
222        assert!(
223            mask.contains(wgpu::Backends::VULKAN) || mask.contains(wgpu::Backends::DX12),
224            "gpu_backends() must include VULKAN or DX12 on Windows. mask = {mask:?}"
225        );
226    }
227
228    #[tokio::test]
229    async fn test_gpu_device_creation() {
230        if !GpuDevice::is_gpu_available().await {
231            eprintln!("⚠️  Skipping test_gpu_device_creation: GPU not available");
232            return;
233        }
234
235        let device = GpuDevice::new().await;
236        assert!(device.is_ok(), "Failed to create GPU device");
237
238        let device = device.unwrap();
239        assert!(device.is_available());
240    }
241
242    #[tokio::test]
243    async fn test_gpu_adapter_info() {
244        if !GpuDevice::is_gpu_available().await {
245            eprintln!("⚠️  Skipping test_gpu_adapter_info: GPU not available");
246            return;
247        }
248
249        let device = GpuDevice::new().await.unwrap();
250        let info = device.info();
251
252        // Basic sanity checks
253        assert!(!info.name.is_empty(), "Adapter name should not be empty");
254        println!("GPU: {info:?}");
255    }
256
257    #[tokio::test]
258    async fn test_gpu_device_with_invalid_backend() {
259        // Try to create device with no backends (should fail)
260        let device = GpuDevice::new_with_backend(wgpu::Backends::empty()).await;
261        assert!(device.is_err(), "Device creation should fail with empty backends");
262    }
263
264    #[test]
265    fn test_gpu_device_error_display() {
266        let err = GpuDeviceError::NoAdapter;
267        assert_eq!(err.to_string(), "No compatible GPU adapter found");
268
269        let err = GpuDeviceError::DeviceRequest("test error".to_string());
270        assert_eq!(err.to_string(), "Failed to request GPU device: test error");
271    }
272
273    #[tokio::test]
274    async fn test_gpu_device_queue() {
275        if !GpuDevice::is_gpu_available().await {
276            eprintln!("⚠️  Skipping test_gpu_device_queue: GPU not available");
277            return;
278        }
279
280        let gpu_device = GpuDevice::new().await.unwrap();
281        let device = gpu_device.device();
282        let queue = gpu_device.queue();
283
284        // Test buffer operations using device and queue
285        let test_data: Vec<u32> = vec![1, 2, 3, 4, 5];
286        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
287            label: Some("test_buffer"),
288            size: (test_data.len() * std::mem::size_of::<u32>()) as u64,
289            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
290            mapped_at_creation: false,
291        });
292
293        queue.write_buffer(&buffer, 0, bytemuck::cast_slice(&test_data));
294        queue.submit(std::iter::empty());
295
296        // Verify device and queue are valid
297        assert!(gpu_device.is_available());
298    }
299
300    #[tokio::test]
301    async fn test_create_buffer_init() {
302        if !GpuDevice::is_gpu_available().await {
303            eprintln!("⚠️  Skipping test_create_buffer_init: GPU not available");
304            return;
305        }
306
307        let device = GpuDevice::new().await.unwrap();
308        let data: Vec<u32> = vec![1, 2, 3, 4];
309
310        let buffer = device
311            .create_buffer_init(
312                "test_init",
313                bytemuck::cast_slice(&data),
314                wgpu::BufferUsages::STORAGE,
315            )
316            .unwrap();
317
318        // Verify buffer was created
319        assert_eq!(buffer.size(), (data.len() * 4) as u64);
320    }
321
322    #[tokio::test]
323    async fn test_create_buffer() {
324        if !GpuDevice::is_gpu_available().await {
325            eprintln!("⚠️  Skipping test_create_buffer: GPU not available");
326            return;
327        }
328
329        let device = GpuDevice::new().await.unwrap();
330
331        // Create empty buffer
332        let buffer = device
333            .create_buffer(
334                "test_buffer",
335                1024,
336                wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
337            )
338            .unwrap();
339
340        assert_eq!(buffer.size(), 1024);
341    }
342
343    #[tokio::test]
344    async fn test_different_buffer_usages() {
345        if !GpuDevice::is_gpu_available().await {
346            eprintln!("⚠️  Skipping test_different_buffer_usages: GPU not available");
347            return;
348        }
349
350        let device = GpuDevice::new().await.unwrap();
351
352        // Storage buffer
353        let storage = device.create_buffer("storage", 512, wgpu::BufferUsages::STORAGE).unwrap();
354        assert_eq!(storage.size(), 512);
355
356        // Uniform buffer
357        let uniform = device.create_buffer("uniform", 256, wgpu::BufferUsages::UNIFORM).unwrap();
358        assert_eq!(uniform.size(), 256);
359
360        // Vertex buffer
361        let vertex = device.create_buffer("vertex", 128, wgpu::BufferUsages::VERTEX).unwrap();
362        assert_eq!(vertex.size(), 128);
363    }
364}