trueno_graph/gpu/
device.rs1use thiserror::Error;
6use wgpu::util::DeviceExt;
7
8#[must_use]
26pub const fn gpu_backends() -> wgpu::Backends {
27 wgpu::Backends::PRIMARY
29}
30
31#[derive(Debug, Error)]
33pub enum GpuDeviceError {
34 #[error("No compatible GPU adapter found")]
36 NoAdapter,
37
38 #[error("Failed to request GPU device: {0}")]
40 DeviceRequest(String),
41
42 #[error("GPU feature not supported: {0}")]
44 UnsupportedFeature(String),
45}
46
47#[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 pub async fn is_gpu_available() -> bool {
71 Self::new().await.is_ok()
72 }
73
74 pub async fn new() -> Result<Self, GpuDeviceError> {
83 Self::new_with_backend(gpu_backends()).await
87 }
88
89 pub async fn new_with_backend(backends: wgpu::Backends) -> Result<Self, GpuDeviceError> {
95 let instance =
97 wgpu::Instance::new(wgpu::InstanceDescriptor { backends, ..Default::default() });
98
99 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 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 #[must_use]
128 pub fn is_available(&self) -> bool {
129 true }
131
132 #[must_use]
134 pub fn info(&self) -> wgpu::AdapterInfo {
135 self.adapter.get_info()
136 }
137
138 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 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 #[must_use]
177 pub const fn device(&self) -> &wgpu::Device {
178 &self.device
179 }
180
181 #[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 #[test]
200 fn test_gpu_backends_excludes_gles() {
201 let mask = gpu_backends();
202
203 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 #[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 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 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 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 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 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 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 let storage = device.create_buffer("storage", 512, wgpu::BufferUsages::STORAGE).unwrap();
354 assert_eq!(storage.size(), 512);
355
356 let uniform = device.create_buffer("uniform", 256, wgpu::BufferUsages::UNIFORM).unwrap();
358 assert_eq!(uniform.size(), 256);
359
360 let vertex = device.create_buffer("vertex", 128, wgpu::BufferUsages::VERTEX).unwrap();
362 assert_eq!(vertex.size(), 128);
363 }
364}