1use crate::{Error, Result};
18use arrow::array::{Array, Float32Array, Int32Array};
19use wgpu;
20use wgpu::util::DeviceExt;
21
22pub mod jit;
23pub mod kernels;
24pub mod multigpu;
25
26#[must_use]
47pub const fn gpu_backends() -> wgpu::Backends {
48 wgpu::Backends::PRIMARY
50}
51
52pub struct GpuEngine {
54 pub device: wgpu::Device,
56 pub queue: wgpu::Queue,
58 jit: jit::JitCompiler,
60}
61
62impl GpuEngine {
63 pub async fn new() -> Result<Self> {
68 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
72 backends: gpu_backends(),
73 ..Default::default()
74 });
75
76 let adapter = instance
77 .request_adapter(&wgpu::RequestAdapterOptions {
78 power_preference: wgpu::PowerPreference::HighPerformance,
79 compatible_surface: None,
80 force_fallback_adapter: false,
81 })
82 .await
83 .ok_or_else(|| Error::GpuInitFailed("No GPU adapter found".to_string()))?;
84
85 let (device, queue) = adapter
87 .request_device(
88 &wgpu::DeviceDescriptor {
89 label: Some("Trueno-DB GPU Device"),
90 required_features: wgpu::Features::empty(),
91 required_limits: wgpu::Limits::default(),
92 memory_hints: wgpu::MemoryHints::default(),
93 },
94 None,
95 )
96 .await
97 .map_err(|e| Error::GpuInitFailed(format!("Failed to create device: {e}")))?;
98
99 Ok(Self { device, queue, jit: jit::JitCompiler::new() })
100 }
101
102 pub async fn sum_i32(&self, data: &Int32Array) -> Result<i32> {
113 kernels::sum_i32(&self.device, &self.queue, data).await
114 }
115
116 pub async fn sum_f32(&self, data: &Float32Array) -> Result<f32> {
121 kernels::sum_f32(&self.device, &self.queue, data).await
122 }
123
124 pub async fn count(&self, data: &dyn Array) -> Result<usize> {
129 kernels::count(&self.device, &self.queue, data).await
130 }
131
132 pub async fn min_i32(&self, data: &Int32Array) -> Result<i32> {
137 kernels::min_i32(&self.device, &self.queue, data).await
138 }
139
140 pub async fn max_i32(&self, data: &Int32Array) -> Result<i32> {
145 kernels::max_i32(&self.device, &self.queue, data).await
146 }
147
148 #[allow(clippy::cast_precision_loss)]
153 pub async fn avg_f32(&self, data: &Float32Array) -> Result<f32> {
154 let sum = self.sum_f32(data).await?;
155 let count = self.count(data).await?;
156 if count == 0 {
157 Ok(0.0)
158 } else {
159 Ok(sum / count as f32)
160 }
161 }
162
163 #[allow(clippy::too_many_lines)]
185 #[allow(clippy::cast_possible_truncation)]
186 pub async fn fused_filter_sum(
187 &self,
188 data: &Int32Array,
189 filter_threshold: i32,
190 filter_op: &str,
191 ) -> Result<i32> {
192 let shader_module =
194 self.jit.compile_fused_filter_sum(&self.device, filter_threshold, filter_op);
195
196 let input_data: Vec<i32> = data.values().to_vec();
198 let input_size = input_data.len();
199
200 if input_size == 0 {
201 return Ok(0);
202 }
203
204 let input_buffer = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
206 label: Some("Fused Filter+Sum Input"),
207 contents: bytemuck::cast_slice(&input_data),
208 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
209 });
210
211 let output_buffer = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
212 label: Some("Fused Filter+Sum Output"),
213 contents: bytemuck::cast_slice(&[0i32]),
214 usage: wgpu::BufferUsages::STORAGE
215 | wgpu::BufferUsages::COPY_SRC
216 | wgpu::BufferUsages::COPY_DST,
217 });
218
219 let bind_group_layout =
221 self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
222 label: Some("Fused Filter+Sum Bind Group Layout"),
223 entries: &[
224 wgpu::BindGroupLayoutEntry {
225 binding: 0,
226 visibility: wgpu::ShaderStages::COMPUTE,
227 ty: wgpu::BindingType::Buffer {
228 ty: wgpu::BufferBindingType::Storage { read_only: true },
229 has_dynamic_offset: false,
230 min_binding_size: None,
231 },
232 count: None,
233 },
234 wgpu::BindGroupLayoutEntry {
235 binding: 1,
236 visibility: wgpu::ShaderStages::COMPUTE,
237 ty: wgpu::BindingType::Buffer {
238 ty: wgpu::BufferBindingType::Storage { read_only: false },
239 has_dynamic_offset: false,
240 min_binding_size: None,
241 },
242 count: None,
243 },
244 ],
245 });
246
247 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
249 label: Some("Fused Filter+Sum Bind Group"),
250 layout: &bind_group_layout,
251 entries: &[
252 wgpu::BindGroupEntry { binding: 0, resource: input_buffer.as_entire_binding() },
253 wgpu::BindGroupEntry { binding: 1, resource: output_buffer.as_entire_binding() },
254 ],
255 });
256
257 let pipeline_layout = self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
259 label: Some("Fused Filter+Sum Pipeline Layout"),
260 bind_group_layouts: &[&bind_group_layout],
261 push_constant_ranges: &[],
262 });
263
264 let compute_pipeline =
265 self.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
266 label: Some("Fused Filter+Sum Pipeline"),
267 layout: Some(&pipeline_layout),
268 module: &shader_module,
269 entry_point: "fused_filter_sum",
270 compilation_options: wgpu::PipelineCompilationOptions::default(),
271 cache: None,
272 });
273
274 let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
276 label: Some("Fused Filter+Sum Encoder"),
277 });
278
279 {
280 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
281 label: Some("Fused Filter+Sum Pass"),
282 timestamp_writes: None,
283 });
284 compute_pass.set_pipeline(&compute_pipeline);
285 compute_pass.set_bind_group(0, &bind_group, &[]);
286
287 let workgroup_count = (input_size as u32).div_ceil(256);
289 compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
290 }
291
292 let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
294 label: Some("Fused Filter+Sum Staging Buffer"),
295 size: 4,
296 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
297 mapped_at_creation: false,
298 });
299
300 encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, 4);
301
302 self.queue.submit(Some(encoder.finish()));
304
305 let buffer_slice = staging_buffer.slice(..);
307 let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
308 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
309 tx.send(result).ok();
310 });
311
312 self.device.poll(wgpu::Maintain::Wait);
313
314 rx.receive()
315 .await
316 .ok_or_else(|| Error::Other("Failed to receive buffer map result".to_string()))?
317 .map_err(|e| Error::Other(format!("Buffer mapping failed: {e}")))?;
318
319 let data_view = buffer_slice.get_mapped_range();
320 let result = i32::from_le_bytes([data_view[0], data_view[1], data_view[2], data_view[3]]);
321
322 drop(data_view);
323 staging_buffer.unmap();
324
325 Ok(result)
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use arrow::array::Int32Array;
333
334 #[test]
342 fn test_gpu_backends_excludes_gles() {
343 let mask = gpu_backends();
344
345 assert!(
347 !mask.contains(wgpu::Backends::GL),
348 "gpu_backends() must NOT include Backends::GL (GLES/EGL panics in Drop \
349 on Linux/AMD-RADV → SIGABRT). mask = {mask:?}"
350 );
351
352 #[cfg(any(target_os = "linux", target_os = "android"))]
354 assert!(
355 mask.contains(wgpu::Backends::VULKAN),
356 "gpu_backends() must include VULKAN on Linux (AMD-RADV/NVIDIA). mask = {mask:?}"
357 );
358 #[cfg(target_os = "macos")]
359 assert!(
360 mask.contains(wgpu::Backends::METAL),
361 "gpu_backends() must include METAL on macOS (Apple Silicon). mask = {mask:?}"
362 );
363 #[cfg(target_os = "windows")]
364 assert!(
365 mask.contains(wgpu::Backends::VULKAN) || mask.contains(wgpu::Backends::DX12),
366 "gpu_backends() must include VULKAN or DX12 on Windows. mask = {mask:?}"
367 );
368 }
369
370 #[tokio::test]
371 async fn test_gpu_init() {
372 match GpuEngine::new().await {
374 Ok(_engine) => {
375 }
377 Err(e) => {
378 eprintln!("GPU initialization failed (expected on CI): {e}");
380 }
381 }
382 }
383
384 #[tokio::test]
385 async fn test_gpu_sum_basic() {
386 let Ok(engine) = GpuEngine::new().await else {
387 eprintln!("Skipping GPU test (no GPU available)");
388 return;
389 };
390
391 let data = Int32Array::from(vec![1, 2, 3, 4, 5]);
392 let result = engine.sum_i32(&data).await.unwrap();
393 assert_eq!(result, 15);
394 }
395
396 #[tokio::test]
397 async fn test_gpu_sum_empty() {
398 let Ok(engine) = GpuEngine::new().await else {
399 eprintln!("Skipping GPU test (no GPU available)");
400 return;
401 };
402
403 let data = Int32Array::from(vec![] as Vec<i32>);
404 let result = engine.sum_i32(&data).await.unwrap();
405 assert_eq!(result, 0);
406 }
407
408 #[tokio::test]
409 async fn test_gpu_min_i32() {
410 let Ok(engine) = GpuEngine::new().await else {
411 eprintln!("Skipping GPU test (no GPU available)");
412 return;
413 };
414
415 let data = Int32Array::from(vec![5, 2, 8, 1, 9]);
416 let result = engine.min_i32(&data).await.unwrap();
417 assert_eq!(result, 1);
418 }
419
420 #[tokio::test]
421 async fn test_gpu_min_empty() {
422 let Ok(engine) = GpuEngine::new().await else {
423 eprintln!("Skipping GPU test (no GPU available)");
424 return;
425 };
426
427 let data = Int32Array::from(vec![] as Vec<i32>);
428 let result = engine.min_i32(&data).await.unwrap();
429 assert_eq!(result, i32::MAX);
430 }
431
432 #[tokio::test]
433 async fn test_gpu_max_i32() {
434 let Ok(engine) = GpuEngine::new().await else {
435 eprintln!("Skipping GPU test (no GPU available)");
436 return;
437 };
438
439 let data = Int32Array::from(vec![5, 2, 8, 1, 9]);
440 let result = engine.max_i32(&data).await.unwrap();
441 assert_eq!(result, 9);
442 }
443
444 #[tokio::test]
445 async fn test_gpu_max_empty() {
446 let Ok(engine) = GpuEngine::new().await else {
447 eprintln!("Skipping GPU test (no GPU available)");
448 return;
449 };
450
451 let data = Int32Array::from(vec![] as Vec<i32>);
452 let result = engine.max_i32(&data).await.unwrap();
453 assert_eq!(result, i32::MIN);
454 }
455
456 #[tokio::test]
457 async fn test_gpu_count() {
458 let Ok(engine) = GpuEngine::new().await else {
459 eprintln!("Skipping GPU test (no GPU available)");
460 return;
461 };
462
463 let data = Int32Array::from(vec![1, 2, 3, 4, 5]);
464 let result = engine.count(&data).await.unwrap();
465 assert_eq!(result, 5);
466 }
467
468 #[tokio::test]
469 async fn test_gpu_sum_f32_not_implemented() {
470 let Ok(engine) = GpuEngine::new().await else {
471 eprintln!("Skipping GPU test (no GPU available)");
472 return;
473 };
474
475 let data = Float32Array::from(vec![1.0, 2.0, 3.0]);
476 let result = engine.sum_f32(&data).await;
477 assert!(result.is_err());
478 assert!(result.unwrap_err().to_string().contains("not yet implemented"));
479 }
480
481 #[tokio::test]
482 async fn test_gpu_avg_f32_not_implemented() {
483 let Ok(engine) = GpuEngine::new().await else {
484 eprintln!("Skipping GPU test (no GPU available)");
485 return;
486 };
487
488 let data = Float32Array::from(vec![2.0, 4.0, 6.0]);
489 let result = engine.avg_f32(&data).await;
490 assert!(result.is_err());
492 }
493
494 #[tokio::test]
495 async fn test_gpu_fused_filter_sum_gt() {
496 let Ok(engine) = GpuEngine::new().await else {
497 eprintln!("Skipping GPU test (no GPU available)");
498 return;
499 };
500
501 let data = Int32Array::from(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
505 let result = engine.fused_filter_sum(&data, 5, "gt").await.unwrap();
506 assert_eq!(result, 40);
507 }
508
509 #[tokio::test]
510 async fn test_gpu_fused_filter_sum_lt() {
511 let Ok(engine) = GpuEngine::new().await else {
512 eprintln!("Skipping GPU test (no GPU available)");
513 return;
514 };
515
516 let data = Int32Array::from(vec![1, 2, 3, 4, 5]);
520 let result = engine.fused_filter_sum(&data, 4, "lt").await.unwrap();
521 assert_eq!(result, 6);
522 }
523
524 #[tokio::test]
525 async fn test_gpu_fused_filter_sum_eq() {
526 let Ok(engine) = GpuEngine::new().await else {
527 eprintln!("Skipping GPU test (no GPU available)");
528 return;
529 };
530
531 let data = Int32Array::from(vec![1, 5, 5, 3, 5]);
535 let result = engine.fused_filter_sum(&data, 5, "eq").await.unwrap();
536 assert_eq!(result, 15);
537 }
538
539 #[tokio::test]
540 async fn test_gpu_fused_filter_sum_empty() {
541 let Ok(engine) = GpuEngine::new().await else {
542 eprintln!("Skipping GPU test (no GPU available)");
543 return;
544 };
545
546 let data = Int32Array::from(vec![] as Vec<i32>);
547 let result = engine.fused_filter_sum(&data, 5, "gt").await.unwrap();
548 assert_eq!(result, 0);
549 }
550
551 #[tokio::test]
552 async fn test_gpu_fused_filter_sum_no_matches() {
553 let Ok(engine) = GpuEngine::new().await else {
554 eprintln!("Skipping GPU test (no GPU available)");
555 return;
556 };
557
558 let data = Int32Array::from(vec![1, 2, 3, 4, 5]);
560 let result = engine.fused_filter_sum(&data, 100, "gt").await.unwrap();
561 assert_eq!(result, 0);
562 }
563}