1use std::collections::HashMap;
6#[cfg(feature = "wgpu")]
7use std::sync::{Arc, Mutex};
9
10use crate::gpu::{GpuBufferImpl, GpuCompilerImpl, GpuContextImpl, GpuError, GpuKernelImpl};
11
12#[cfg(feature = "wgpu")]
13#[allow(unused_imports)]
14use wgpu::{
15 util::DeviceExt, Backends, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
16 BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, Buffer,
17 BufferBindingType, BufferDescriptor, BufferUsages, ComputePipeline, Device, DeviceDescriptor,
18 Features, Instance, InstanceDescriptor, Limits, PowerPreference, Queue, RequestAdapterOptions,
19 ShaderModuleDescriptor, ShaderSource, ShaderStages, StorageTextureAccess, TextureFormat,
20 TextureSampleType, TextureViewDimension,
21};
22
23#[cfg(not(feature = "wgpu"))]
25type WgpuDevice = *mut std::ffi::c_void;
26#[cfg(not(feature = "wgpu"))]
27type WgpuQueue = *mut std::ffi::c_void;
28#[cfg(not(feature = "wgpu"))]
29type WgpuBuffer = *mut std::ffi::c_void;
30#[cfg(not(feature = "wgpu"))]
31type WgpuComputePipeline = *mut std::ffi::c_void;
32
33#[cfg(feature = "wgpu")]
38pub struct WgpuComputePipeline {
39 pub pipeline: ComputePipeline,
41 pub bind_group_layout: BindGroupLayout,
43 pub workgroup_size: [u32; 3],
45}
46
47#[cfg(feature = "wgpu")]
48unsafe impl Send for WgpuComputePipeline {}
50#[cfg(feature = "wgpu")]
51unsafe impl Sync for WgpuComputePipeline {}
52
53#[cfg(feature = "wgpu")]
74pub fn try_compile_wgsl(source: &str) -> Result<WgpuComputePipeline, GpuError> {
75 let ctx = WebGPUContext::new()?;
76 ctx.compile_to_pipeline(source)
77}
78
79#[cfg(feature = "wgpu")]
84pub fn run_vector_add_wgsl(a: &[f32], b: &[f32]) -> Result<Vec<f32>, GpuError> {
85 let ctx = WebGPUContext::new()?;
86 ctx.run_vector_add(a, b)
87}
88
89pub const GEMM_SHADER_WGSL: &str = r#"
99@group(0) @binding(0) var<storage, read> matrix_a: array<f32>;
100@group(0) @binding(1) var<storage, read> matrix_b: array<f32>;
101@group(0) @binding(2) var<storage, read_write> matrix_c: array<f32>;
102
103struct GemmUniforms {
104 M: u32,
105 N: u32,
106 K: u32,
107 alpha: f32,
108 beta: f32,
109};
110
111@group(0) @binding(3) var<uniform> uniforms: GemmUniforms;
112
113@compute @workgroup_size(8, 8)
114fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
115 let row = global_id.x;
116 let col = global_id.y;
117
118 if row >= uniforms.M || col >= uniforms.N { return; }
119
120 var sum = 0.0f;
121 for (var k = 0u; k < uniforms.K; k++) {
122 sum += matrix_a[row * uniforms.K + k] * matrix_b[k * uniforms.N + col];
123 }
124
125 let idx = row * uniforms.N + col;
126 matrix_c[idx] = uniforms.alpha * sum + uniforms.beta * matrix_c[idx];
127}
128"#;
129
130const VECTOR_ADD_SHADER_WGSL: &str = r#"
139@group(0) @binding(0) var<storage, read> a : array<f32>;
140@group(0) @binding(1) var<storage, read> b : array<f32>;
141@group(0) @binding(2) var<storage, read_write> result : array<f32>;
142
143@compute @workgroup_size(64)
144fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
145 let idx = global_id.x;
146 if idx < arrayLength(&result) {
147 result[idx] = a[idx] + b[idx];
148 }
149}
150"#;
151
152pub struct WebGPUContext {
154 #[cfg(feature = "wgpu")]
155 device: Arc<Device>,
156 #[cfg(feature = "wgpu")]
157 queue: Arc<Queue>,
158 #[cfg(not(feature = "wgpu"))]
159 device: Arc<WgpuDevice>,
160 #[cfg(not(feature = "wgpu"))]
161 queue: Arc<WgpuQueue>,
162 compiled_shaders: Arc<Mutex<HashMap<String, WebGPUShader>>>,
163 memory_pool: Arc<Mutex<WebGPUMemoryPool>>,
164}
165
166unsafe impl Send for WebGPUContext {}
168unsafe impl Sync for WebGPUContext {}
169
170impl WebGPUContext {
171 pub fn new() -> Result<Self, GpuError> {
173 #[cfg(feature = "wgpu")]
174 {
175 let instance_desc = InstanceDescriptor {
185 backends: Backends::PRIMARY,
186 flags: wgpu::InstanceFlags::default(),
187 memory_budget_thresholds: Default::default(),
188 backend_options: Default::default(),
189 display: None,
190 };
191 let instance = Instance::new(instance_desc);
192
193 let adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
194 power_preference: PowerPreference::HighPerformance,
195 compatible_surface: None,
196 force_fallback_adapter: false,
197 }))
198 .map_err(|e| GpuError::Other(format!("Failed to find WebGPU adapter: {e}")))?;
199
200 let device_descriptor = DeviceDescriptor {
201 label: Some("SciRS2 WebGPU Device"),
202 required_features: Features::empty(),
203 required_limits: Limits::default(),
204 ..Default::default()
206 };
207
208 let (device, queue) = pollster::block_on(adapter.request_device(&device_descriptor))
209 .map_err(|e| GpuError::Other(format!("{e}")))?;
210
211 Ok(Self {
212 device: Arc::new(device),
213 queue: Arc::new(queue),
214 compiled_shaders: Arc::new(Mutex::new(HashMap::new())),
215 memory_pool: Arc::new(Mutex::new(WebGPUMemoryPool::new(1024 * 1024 * 1024))), })
217 }
218 #[cfg(not(feature = "wgpu"))]
219 {
220 Err(GpuError::BackendNotAvailable(
225 "WebGPU support was not compiled into this build (the `wgpu` Cargo feature is \
226 disabled); enable it to use the WebGPU backend"
227 .to_string(),
228 ))
229 }
230 }
231
232 pub fn is_available() -> bool {
234 #[cfg(feature = "wgpu")]
235 {
236 let instance_desc = InstanceDescriptor {
239 backends: Backends::PRIMARY,
240 flags: wgpu::InstanceFlags::default(),
241 memory_budget_thresholds: Default::default(),
242 backend_options: Default::default(),
243 display: None,
244 };
245 let instance = Instance::new(instance_desc);
246
247 pollster::block_on(async {
249 instance
250 .request_adapter(&RequestAdapterOptions {
251 power_preference: PowerPreference::default(),
252 compatible_surface: None,
253 force_fallback_adapter: false,
254 })
255 .await
256 .is_ok()
257 })
258 }
259 #[cfg(not(feature = "wgpu"))]
260 {
261 false
263 }
264 }
265
266 fn compile_shader_internal(&self, source: &str, name: &str) -> Result<WebGPUShader, GpuError> {
268 #[cfg(feature = "wgpu")]
269 {
270 let shader_module = self.device.create_shader_module(ShaderModuleDescriptor {
272 label: Some(name),
273 source: ShaderSource::Wgsl(source.into()),
274 });
275
276 let entry_point = Self::extract_entry_point(source).unwrap_or("main");
278
279 let (bind_group_layout, binding_infos) =
281 self.create_bind_group_layout_from_source(source, name)?;
282
283 let pipeline_layout =
285 self.device
286 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
287 label: Some(&format!("{}_layout", name)),
288 bind_group_layouts: &[Some(&bind_group_layout)],
289 ..Default::default()
291 });
292
293 let compute_pipeline =
294 self.device
295 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
296 label: Some(&format!("{}_pipeline", name)),
297 layout: Some(&pipeline_layout),
298 module: &shader_module,
299 entry_point: Some(entry_point),
300 compilation_options: Default::default(),
301 cache: None,
302 });
303
304 let workgroup_size = extract_workgroup_size(source);
305
306 Ok(WebGPUShader {
307 pipeline: compute_pipeline,
308 bind_group_layout,
309 name: name.to_string(),
310 binding_infos,
311 workgroup_size,
312 })
313 }
314 #[cfg(not(feature = "wgpu"))]
315 {
316 let pipeline = Self::compile_wgsl_source(source, name)?;
318
319 Ok(WebGPUShader {
320 pipeline,
321 bind_group_layout: std::ptr::null_mut(),
322 name: name.to_string(),
323 binding_infos: Vec::new(),
324 workgroup_size: [64, 1, 1],
325 })
326 }
327 }
328
329 #[cfg(feature = "wgpu")]
331 fn create_bind_group_layout_from_source(
332 &self,
333 source: &str,
334 name: &str,
335 ) -> Result<(BindGroupLayout, Vec<BindingInfo>), GpuError> {
336 #[derive(Default)]
337 struct PendingAttr {
338 group: Option<u32>,
339 binding: Option<u32>,
340 }
341 let mut pending = PendingAttr::default();
342 let mut entries: Vec<BindGroupLayoutEntry> = Vec::new();
343 let mut infos: Vec<BindingInfo> = Vec::new();
344
345 fn strip_comment(line: &str) -> &str {
346 line.split_once("//").map(|(a, _)| a).unwrap_or(line)
347 }
348
349 for raw_line in source.lines() {
350 let line = strip_comment(raw_line).trim();
351 if line.is_empty() {
352 continue;
353 }
354
355 if let Some(i) = line.find("@group(") {
356 if let Some(end) = line[i + 7..].find(')') {
357 if let Ok(g) = line[i + 7..i + 7 + end].parse::<u32>() {
358 pending.group = Some(g);
359 }
360 }
361 }
362 if let Some(i) = line.find("@binding(") {
363 if let Some(end) = line[i + 9..].find(')') {
364 if let Ok(b) = line[i + 9..i + 9 + end].parse::<u32>() {
365 pending.binding = Some(b);
366 }
367 }
368 }
369
370 if line.contains("var<") {
371 if pending.group.unwrap_or(0) == 0 {
373 let binding_num = pending.binding.unwrap_or_else(|| entries.len() as u32);
375 let name = extract_var_name(line).unwrap_or("");
376 let storage = line.contains("var<storage");
377 let uniform = line.contains("var<uniform");
378 let read_only = storage
379 && (line.contains(", read>")
380 || line.contains("var<storage, read>")
381 || line.contains("var<storage, read,"));
382 if storage {
383 entries.push(BindGroupLayoutEntry {
384 binding: binding_num,
385 visibility: ShaderStages::COMPUTE,
386 ty: BindingType::Buffer {
387 ty: BufferBindingType::Storage { read_only },
388 has_dynamic_offset: false,
389 min_binding_size: None,
390 },
391 count: None,
392 });
393 infos.push(BindingInfo {
394 binding: binding_num,
395 name: name.to_string(),
396 kind: if read_only {
397 BindingKind::StorageRead
398 } else {
399 BindingKind::StorageRw
400 },
401 });
402 } else if uniform {
403 entries.push(BindGroupLayoutEntry {
404 binding: binding_num,
405 visibility: ShaderStages::COMPUTE,
406 ty: BindingType::Buffer {
407 ty: BufferBindingType::Uniform,
408 has_dynamic_offset: false,
409 min_binding_size: None,
410 },
411 count: None,
412 });
413 infos.push(BindingInfo {
414 binding: binding_num,
415 name: name.to_string(),
416 kind: BindingKind::Uniform,
417 });
418 }
419 }
420 pending = PendingAttr::default();
421 }
422 }
423
424 if entries.is_empty() {
425 entries.push(BindGroupLayoutEntry {
426 binding: 0,
427 visibility: ShaderStages::COMPUTE,
428 ty: BindingType::Buffer {
429 ty: BufferBindingType::Storage { read_only: false },
430 has_dynamic_offset: false,
431 min_binding_size: None,
432 },
433 count: None,
434 });
435 infos.push(BindingInfo {
436 binding: 0,
437 name: "_unnamed".into(),
438 kind: BindingKind::StorageRw,
439 });
440 }
441
442 let mut seen = std::collections::HashSet::new();
444 let mut dedup_entries = Vec::new();
445 let mut dedup_infos = Vec::new();
446 for (e, info) in entries.into_iter().zip(infos) {
447 if seen.insert(e.binding) {
448 dedup_entries.push(e);
449 dedup_infos.push(info);
450 }
451 }
452
453 let bind_group_layout = self
454 .device
455 .create_bind_group_layout(&BindGroupLayoutDescriptor {
456 label: Some(&format!("{}_bind_group_layout", name)),
457 entries: &dedup_entries,
458 });
459 Ok((bind_group_layout, dedup_infos))
460 }
461
462 #[cfg(feature = "wgpu")]
464 pub fn device(&self) -> &Device {
465 &self.device
466 }
467
468 #[cfg(feature = "wgpu")]
470 pub fn queue(&self) -> &Queue {
471 &self.queue
472 }
473
474 #[cfg(feature = "wgpu")]
476 pub fn allocate_device_memory(&self, size: usize) -> Result<Buffer, GpuError> {
477 let buffer = self.device.create_buffer(&BufferDescriptor {
478 label: Some("SciRS2 Buffer"),
479 size: size as u64,
480 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
481 mapped_at_creation: false,
482 });
483
484 Ok(buffer)
485 }
486
487 #[cfg(not(feature = "wgpu"))]
496 fn compile_wgsl_source(_source: &str, _name: &str) -> Result<WgpuComputePipeline, GpuError> {
497 Err(GpuError::BackendNotAvailable(
498 "WebGPU support was not compiled into this build (the `wgpu` Cargo feature is \
499 disabled); no shader can be compiled"
500 .to_string(),
501 ))
502 }
503
504 #[cfg(feature = "wgpu")]
510 pub fn compile_to_pipeline(&self, source: &str) -> Result<WgpuComputePipeline, GpuError> {
511 let shader = self.compile_shader_internal(source, "scirs2-pipeline")?;
512 Ok(WgpuComputePipeline {
513 pipeline: shader.pipeline,
514 bind_group_layout: shader.bind_group_layout,
515 workgroup_size: shader.workgroup_size,
516 })
517 }
518
519 #[cfg(feature = "wgpu")]
521 pub fn run_vector_add(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>, GpuError> {
522 use wgpu::{util::DeviceExt as _, BufferUsages};
523
524 let n = a.len();
525 if n != b.len() {
526 return Err(GpuError::InvalidParameter(
527 "vectors must have equal length".into(),
528 ));
529 }
530
531 let shader_module = self.device.create_shader_module(ShaderModuleDescriptor {
533 label: Some("vector-add"),
534 source: ShaderSource::Wgsl(VECTOR_ADD_SHADER_WGSL.into()),
535 });
536
537 let bgl = self
539 .device
540 .create_bind_group_layout(&BindGroupLayoutDescriptor {
541 label: Some("vector-add-bgl"),
542 entries: &[
543 BindGroupLayoutEntry {
545 binding: 0,
546 visibility: ShaderStages::COMPUTE,
547 ty: BindingType::Buffer {
548 ty: BufferBindingType::Storage { read_only: true },
549 has_dynamic_offset: false,
550 min_binding_size: None,
551 },
552 count: None,
553 },
554 BindGroupLayoutEntry {
556 binding: 1,
557 visibility: ShaderStages::COMPUTE,
558 ty: BindingType::Buffer {
559 ty: BufferBindingType::Storage { read_only: true },
560 has_dynamic_offset: false,
561 min_binding_size: None,
562 },
563 count: None,
564 },
565 BindGroupLayoutEntry {
567 binding: 2,
568 visibility: ShaderStages::COMPUTE,
569 ty: BindingType::Buffer {
570 ty: BufferBindingType::Storage { read_only: false },
571 has_dynamic_offset: false,
572 min_binding_size: None,
573 },
574 count: None,
575 },
576 ],
577 });
578
579 let pipeline_layout = self
580 .device
581 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
582 label: Some("vector-add-layout"),
583 bind_group_layouts: &[Some(&bgl)],
584 ..Default::default()
585 });
586
587 let pipeline = self
588 .device
589 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
590 label: Some("vector-add-pipeline"),
591 layout: Some(&pipeline_layout),
592 module: &shader_module,
593 entry_point: Some("main"),
594 compilation_options: Default::default(),
595 cache: None,
596 });
597
598 let a_bytes: Vec<u8> = a.iter().flat_map(|f| f.to_le_bytes()).collect();
600 let b_bytes: Vec<u8> = b.iter().flat_map(|f| f.to_le_bytes()).collect();
601 let result_size = std::mem::size_of_val(a) as u64;
602
603 let buf_a = self
604 .device
605 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
606 label: Some("vector-add-a"),
607 contents: &a_bytes,
608 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC,
609 });
610 let buf_b = self
611 .device
612 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
613 label: Some("vector-add-b"),
614 contents: &b_bytes,
615 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC,
616 });
617 let buf_result = self.device.create_buffer(&BufferDescriptor {
618 label: Some("vector-add-result"),
619 size: result_size,
620 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
621 mapped_at_creation: false,
622 });
623
624 let bind_group = self.device.create_bind_group(&BindGroupDescriptor {
626 label: Some("vector-add-bg"),
627 layout: &bgl,
628 entries: &[
629 BindGroupEntry {
630 binding: 0,
631 resource: buf_a.as_entire_binding(),
632 },
633 BindGroupEntry {
634 binding: 1,
635 resource: buf_b.as_entire_binding(),
636 },
637 BindGroupEntry {
638 binding: 2,
639 resource: buf_result.as_entire_binding(),
640 },
641 ],
642 });
643
644 let mut encoder = self
646 .device
647 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
648 label: Some("vector-add-encoder"),
649 });
650 {
651 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
652 label: Some("vector-add-pass"),
653 timestamp_writes: None,
654 });
655 cpass.set_pipeline(&pipeline);
656 cpass.set_bind_group(0, &bind_group, &[]);
657 let workgroups = (n as u32 + 63) / 64;
658 cpass.dispatch_workgroups(workgroups, 1, 1);
659 }
660
661 let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
663 label: Some("vector-add-staging"),
664 size: result_size,
665 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
666 mapped_at_creation: false,
667 });
668 encoder.copy_buffer_to_buffer(&buf_result, 0, &staging, 0, result_size);
669 self.queue.submit(Some(encoder.finish()));
670
671 self.device
673 .poll(wgpu::PollType::wait_indefinitely())
674 .map_err(|e| GpuError::Other(format!("GPU poll error: {e:?}")))?;
675
676 let slice = staging.slice(0..result_size);
677 let (tx, rx) = std::sync::mpsc::channel();
678 slice.map_async(wgpu::MapMode::Read, move |r| {
679 let _ = tx.send(r);
680 });
681
682 self.device
684 .poll(wgpu::PollType::wait_indefinitely())
685 .map_err(|e| GpuError::Other(format!("GPU poll error during map: {e:?}")))?;
686
687 rx.recv()
688 .map_err(|_| GpuError::Other("Channel closed during map_async".into()))?
689 .map_err(|e| GpuError::Other(format!("map_async failed: {e:?}")))?;
690
691 let mapped = slice.get_mapped_range();
692 let result: Vec<f32> = mapped
693 .chunks_exact(4)
694 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
695 .collect();
696 drop(mapped);
697 staging.unmap();
698
699 Ok(result)
700 }
701
702 fn extract_entry_point(source: &str) -> Option<&str> {
704 let lines: Vec<&str> = source.lines().collect();
705
706 for (i, line) in lines.iter().enumerate() {
707 let trimmed = line.trim();
708
709 if trimmed.contains("@compute") {
711 let mut search_line = trimmed;
713 let mut search_idx = 0;
714
715 if !search_line.contains("fn ") && search_idx + 1 < lines.len() {
717 search_idx += 1;
718 search_line = lines[search_idx].trim();
719 }
720
721 if let Some(start) = search_line.find("fn ") {
723 let remaining = &search_line[start + 3..];
724 if let Some(end) = remaining.find('(') {
725 let funcname = remaining[..end].trim();
726 return Some(funcname);
727 }
728 }
729 }
730 }
731
732 None
733 }
734}
735
736impl GpuContextImpl for WebGPUContext {
737 fn create_buffer(&self, size: usize) -> Arc<dyn GpuBufferImpl> {
738 if let Ok(mut pool) = self.memory_pool.lock() {
740 if let Some(device_buffer) = pool.allocate(size) {
741 return Arc::new(WebGPUBuffer {
742 device_buffer: Some(device_buffer),
743 #[cfg(feature = "wgpu")]
744 queue: Arc::clone(&self.queue),
745 #[cfg(feature = "wgpu")]
746 device: Arc::clone(&self.device),
747 #[cfg(not(feature = "wgpu"))]
748 queue: self.queue,
749 size,
750 memory_pool: Arc::clone(&self.memory_pool),
751 });
752 }
753 }
754
755 let device_buffer = match self.allocate_device_memory(size) {
757 Ok(buffer) => buffer,
758 Err(e) => {
759 eprintln!(
761 "Warning: WebGPU buffer allocation failed ({}), creating CPU fallback buffer",
762 e
763 );
764
765 #[cfg(feature = "wgpu")]
766 {
767 return Arc::new(WebGPUCpuFallbackBuffer {
770 data: vec![0u8; size],
771 size,
772 memory_pool: Arc::clone(&self.memory_pool),
773 });
774 }
775 #[cfg(not(feature = "wgpu"))]
776 {
777 (0x2000 + size) as WgpuBuffer
778 }
779 }
780 };
781
782 Arc::new(WebGPUBuffer {
783 device_buffer: Some(device_buffer),
784 #[cfg(feature = "wgpu")]
785 queue: Arc::clone(&self.queue),
786 #[cfg(feature = "wgpu")]
787 device: Arc::clone(&self.device),
788 #[cfg(not(feature = "wgpu"))]
789 queue: self.queue,
790 size,
791 memory_pool: Arc::clone(&self.memory_pool),
792 })
793 }
794
795 fn create_compiler(&self) -> Arc<dyn GpuCompilerImpl> {
796 Arc::new(WebGPUCompiler {
797 context: Arc::new(WebGPUContext {
798 memory_pool: Arc::clone(&self.memory_pool),
799 compiled_shaders: Arc::clone(&self.compiled_shaders),
800 #[cfg(feature = "wgpu")]
801 device: Arc::clone(&self.device),
802 #[cfg(feature = "wgpu")]
803 queue: Arc::clone(&self.queue),
804 #[cfg(not(feature = "wgpu"))]
805 device: Arc::clone(&self.device),
806 #[cfg(not(feature = "wgpu"))]
807 queue: Arc::clone(&self.queue),
808 }),
809 })
810 }
811
812 fn as_any(&self) -> &dyn std::any::Any {
813 self
814 }
815}
816
817struct WebGPUShader {
819 #[cfg(feature = "wgpu")]
820 pipeline: ComputePipeline,
821 #[cfg(not(feature = "wgpu"))]
822 pipeline: WgpuComputePipeline,
823 #[cfg(feature = "wgpu")]
824 #[allow(dead_code)]
825 bind_group_layout: BindGroupLayout,
826 #[cfg(not(feature = "wgpu"))]
827 #[allow(dead_code)]
828 bind_group_layout: *mut std::ffi::c_void,
829 #[allow(dead_code)]
830 name: String,
831 #[allow(dead_code)]
832 binding_infos: Vec<BindingInfo>, #[allow(dead_code)]
834 workgroup_size: [u32; 3],
835}
836
837unsafe impl Send for WebGPUShader {}
839unsafe impl Sync for WebGPUShader {}
840
841struct WebGPUCompiler {
843 context: Arc<WebGPUContext>,
844}
845
846impl GpuCompilerImpl for WebGPUCompiler {
847 fn compile(&self, source: &str) -> Result<Arc<dyn GpuKernelImpl>, GpuError> {
848 let shader = self.context.compile_shader_internal(source, "shader")?;
849 let shader_name = shader.name.clone();
850 self.context
854 .compiled_shaders
855 .lock()
856 .map_err(|_| {
857 GpuError::Other("WebGPU compiled-shader registry lock poisoned".to_string())
858 })?
859 .insert(shader_name.clone(), shader);
860
861 Ok(Arc::new(WebGPUKernelHandle {
862 shader_name,
863 compiled_shaders: Arc::clone(&self.context.compiled_shaders),
864 params: Arc::new(Mutex::new(HashMap::new())),
865 #[cfg(feature = "wgpu")]
866 device: Arc::clone(&self.context.device),
867 #[cfg(feature = "wgpu")]
868 queue: Arc::clone(&self.context.queue),
869 #[cfg(feature = "wgpu")]
870 ephemeral_uniforms: Mutex::new(Vec::new()),
871 #[cfg(not(feature = "wgpu"))]
872 device: self.context.device,
873 #[cfg(not(feature = "wgpu"))]
874 queue: self.context.queue,
875 }))
876 }
877
878 fn compile_typed(
879 &self,
880 name: &str,
881 input_type: std::any::TypeId,
882 output_type: std::any::TypeId,
883 ) -> Result<Arc<dyn GpuKernelImpl>, GpuError> {
884 let is_f32 = input_type == std::any::TypeId::of::<f32>()
892 && output_type == std::any::TypeId::of::<f32>();
893 if name != "vector_add" || !is_f32 {
894 return Err(GpuError::KernelCompilationError(format!(
895 "compile_typed has no built-in WGSL source for kernel '{name}' with the \
896 requested types; only a real f32 'vector_add' kernel is available this way. \
897 Use GpuCompiler::compile with real WGSL source, or GpuContext::get_kernel for \
898 registry-backed named kernels, for anything else."
899 )));
900 }
901
902 let shader = self
903 .context
904 .compile_shader_internal(VECTOR_ADD_SHADER_WGSL, name)?;
905 self.context
906 .compiled_shaders
907 .lock()
908 .map_err(|_| {
909 GpuError::Other("WebGPU compiled-shader registry lock poisoned".to_string())
910 })?
911 .insert(name.to_string(), shader);
912
913 Ok(Arc::new(WebGPUKernelHandle {
914 shader_name: name.to_string(),
915 compiled_shaders: Arc::clone(&self.context.compiled_shaders),
916 params: Arc::new(Mutex::new(HashMap::new())),
917 #[cfg(feature = "wgpu")]
918 device: Arc::clone(&self.context.device),
919 #[cfg(feature = "wgpu")]
920 queue: Arc::clone(&self.context.queue),
921 #[cfg(feature = "wgpu")]
922 ephemeral_uniforms: Mutex::new(Vec::new()),
923 #[cfg(not(feature = "wgpu"))]
924 device: self.context.device,
925 #[cfg(not(feature = "wgpu"))]
926 queue: self.context.queue,
927 }))
928 }
929}
930
931struct WebGPUKernelHandle {
933 shader_name: String,
934 compiled_shaders: Arc<Mutex<HashMap<String, WebGPUShader>>>,
935 params: Arc<Mutex<HashMap<String, KernelParam>>>,
936 #[cfg(feature = "wgpu")]
937 device: Arc<Device>,
938 #[cfg(feature = "wgpu")]
939 queue: Arc<Queue>,
940 #[cfg(feature = "wgpu")]
941 ephemeral_uniforms: Mutex<Vec<wgpu::Buffer>>,
942 #[cfg(not(feature = "wgpu"))]
943 device: WgpuDevice,
944 #[cfg(not(feature = "wgpu"))]
945 queue: WgpuQueue,
946}
947
948enum KernelParam {
949 #[allow(dead_code)]
950 Buffer(Arc<dyn GpuBufferImpl>),
951 #[allow(dead_code)]
952 U32(u32),
953 #[allow(dead_code)]
954 I32(i32),
955 #[allow(dead_code)]
956 F32(f32),
957 #[allow(dead_code)]
958 F64(f64),
959 Bytes(Vec<u8>),
960}
961
962#[derive(Clone, Debug)]
963enum BindingKind {
964 StorageRw,
965 StorageRead,
966 Uniform,
967}
968
969#[derive(Clone, Debug)]
970struct BindingInfo {
971 binding: u32,
972 name: String,
973 kind: BindingKind,
974}
975
976fn extract_workgroup_size(source: &str) -> [u32; 3] {
979 for line in source.lines() {
980 let trimmed = line.trim();
981 if let Some(start) = trimmed.find("@workgroup_size(") {
982 let after = &trimmed[start + "@workgroup_size(".len()..];
983 if let Some(end) = after.find(')') {
984 let inner = &after[..end];
985 let parts: Vec<u32> = inner
986 .split(',')
987 .filter_map(|s| s.trim().parse::<u32>().ok())
988 .collect();
989 return match parts.as_slice() {
990 [x] => [*x, 1, 1],
991 [x, y] => [*x, *y, 1],
992 [x, y, z, ..] => [*x, *y, *z],
993 _ => [64, 1, 1],
994 };
995 }
996 }
997 }
998 [64, 1, 1]
999}
1000
1001fn extract_var_name(line: &str) -> Option<&str> {
1002 if let Some(var_start) = line.find("var<") {
1003 let after_var = &line[var_start..];
1004 if let Some(close) = after_var.find('>') {
1005 let after = &after_var[close + 1..];
1006 let after = after.trim_start();
1007 if let Some(colon) = after.find(':') {
1008 let name_part = after[..colon].trim();
1009 if !name_part.is_empty() {
1010 return Some(name_part);
1011 }
1012 }
1013 }
1014 }
1015 None
1016}
1017
1018impl GpuKernelImpl for WebGPUKernelHandle {
1019 fn set_buffer(&self, name: &str, buffer: &Arc<dyn GpuBufferImpl>) {
1020 if let Ok(mut params) = self.params.lock() {
1021 params.insert(name.to_string(), KernelParam::Buffer(Arc::clone(buffer)));
1022 }
1023 }
1024
1025 fn set_u32(&self, name: &str, value: u32) {
1026 if let Ok(mut params) = self.params.lock() {
1027 params.insert(name.to_string(), KernelParam::U32(value));
1028 }
1029 }
1030
1031 fn set_i32(&self, name: &str, value: i32) {
1032 if let Ok(mut params) = self.params.lock() {
1033 params.insert(name.to_string(), KernelParam::I32(value));
1034 }
1035 }
1036
1037 fn set_f32(&self, name: &str, value: f32) {
1038 if let Ok(mut params) = self.params.lock() {
1039 params.insert(name.to_string(), KernelParam::F32(value));
1040 }
1041 }
1042
1043 fn set_f64(&self, name: &str, value: f64) {
1044 if let Ok(mut params) = self.params.lock() {
1045 params.insert(name.to_string(), KernelParam::F64(value));
1046 }
1047 }
1048
1049 #[allow(dead_code)]
1050 fn dispatch(&self, workgroups: [u32; 3]) {
1053 #[cfg(feature = "wgpu")]
1054 {
1055 let shaders = match self.compiled_shaders.lock() {
1057 Ok(g) => g,
1058 Err(_) => return,
1059 };
1060 if let Some(shader) = shaders.get(&self.shader_name) {
1061 let params = match self.params.lock() {
1062 Ok(g) => g,
1063 Err(_) => return,
1064 };
1065
1066 let mut encoder =
1068 self.device
1069 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1070 label: Some("Compute Command Encoder"),
1071 });
1072
1073 {
1075 let mut compute_pass =
1076 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1077 label: Some("Compute Pass"),
1078 timestamp_writes: None,
1079 });
1080
1081 compute_pass.set_pipeline(&shader.pipeline);
1083
1084 if let Ok(bind_group) = self.create_bind_group_from_params(shader, ¶ms) {
1085 compute_pass.set_bind_group(0, &bind_group, &[]);
1086 }
1087 compute_pass.dispatch_workgroups(workgroups[0], workgroups[1], workgroups[2]);
1091 }
1092
1093 let command_buffer = encoder.finish();
1095 self.queue.submit(std::iter::once(command_buffer));
1096 }
1097 }
1098 #[cfg(not(feature = "wgpu"))]
1099 {
1100 let _ = workgroups;
1102 let _ = &self.shader_name;
1103 }
1104 }
1105}
1106
1107struct WebGPUBuffer {
1109 #[cfg(feature = "wgpu")]
1110 device_buffer: Option<Buffer>,
1111 #[cfg(feature = "wgpu")]
1112 queue: Arc<Queue>,
1113 #[cfg(feature = "wgpu")]
1114 device: Arc<Device>,
1115 #[cfg(not(feature = "wgpu"))]
1116 device_buffer: Option<WgpuBuffer>,
1117 #[cfg(not(feature = "wgpu"))]
1118 queue: WgpuQueue,
1119 size: usize,
1120 memory_pool: Arc<Mutex<WebGPUMemoryPool>>,
1121}
1122
1123unsafe impl Send for WebGPUBuffer {}
1127unsafe impl Sync for WebGPUBuffer {}
1128
1129impl GpuBufferImpl for WebGPUBuffer {
1130 fn size(&self) -> usize {
1131 self.size
1132 }
1133
1134 unsafe fn copy_from_host(&self, data: *const u8, size: usize) {
1135 #[cfg(feature = "wgpu")]
1136 {
1137 if size > self.size {
1139 eprintln!(
1141 "Warning: Data size {} exceeds buffer size {}",
1142 size, self.size
1143 );
1144 return;
1145 }
1146
1147 let data_slice = std::slice::from_raw_parts(data, size);
1149
1150 if let Some(ref buffer) = self.device_buffer {
1152 self.queue.write_buffer(buffer, 0, data_slice);
1153 }
1154 }
1155 #[cfg(not(feature = "wgpu"))]
1156 {
1157 if size > self.size {
1159 eprintln!(
1160 "Warning: Data size {} exceeds buffer size {}",
1161 size, self.size
1162 );
1163 }
1164 }
1166 }
1167
1168 unsafe fn copy_to_host(&self, data: *mut u8, size: usize) {
1169 #[cfg(feature = "wgpu")]
1170 {
1171 if size > self.size {
1173 eprintln!(
1174 "Warning: Data size {} exceeds buffer size {}",
1175 size, self.size
1176 );
1177 return;
1178 }
1179
1180 if let Some(ref buffer) = self.device_buffer {
1181 let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
1182 label: Some("scirs2-readback"),
1183 size: size as u64,
1184 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1185 mapped_at_creation: false,
1186 });
1187 let mut encoder =
1188 self.device
1189 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1190 label: Some("scirs2-readback-enc"),
1191 });
1192 encoder.copy_buffer_to_buffer(buffer, 0, &staging, 0, size as u64);
1193 self.queue.submit(Some(encoder.finish()));
1194
1195 let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
1199
1200 let slice = staging.slice(0..size as u64);
1201 let (tx, rx) = std::sync::mpsc::channel();
1202 slice.map_async(wgpu::MapMode::Read, move |r| {
1203 let _ = tx.send(r);
1204 });
1205 let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
1207 if let Ok(Ok(())) = rx.recv() {
1208 let mapped = slice.get_mapped_range();
1209 let dst = std::slice::from_raw_parts_mut(data, size);
1210 dst.copy_from_slice(&mapped);
1211 drop(mapped);
1212 staging.unmap();
1213 } else {
1214 eprintln!("Warning: map_async failed for readback");
1215 }
1216 }
1217 }
1218 #[cfg(not(feature = "wgpu"))]
1219 {
1220 if size > self.size {
1222 eprintln!(
1223 "Warning: Data size {} exceeds buffer size {}",
1224 size, self.size
1225 );
1226 }
1227
1228 let data_slice = std::slice::from_raw_parts_mut(data, size);
1230 data_slice.fill(0);
1231 }
1232 }
1233
1234 fn device_ptr(&self) -> u64 {
1235 #[cfg(feature = "wgpu")]
1236 {
1237 &self.device_buffer as *const _ as u64
1240 }
1241 #[cfg(not(feature = "wgpu"))]
1242 {
1243 self.device_buffer as u64
1244 }
1245 }
1246
1247 fn as_any(&self) -> &dyn std::any::Any {
1248 self
1249 }
1250}
1251
1252#[cfg(feature = "wgpu")]
1253impl WebGPUKernelHandle {
1254 fn create_bind_group_from_params(
1255 &self,
1256 shader: &WebGPUShader,
1257 params: &HashMap<String, KernelParam>,
1258 ) -> Result<wgpu::BindGroup, GpuError> {
1259 let mut entries: Vec<wgpu::BindGroupEntry> = Vec::new();
1260 let mut owned_uniform_buffers: Vec<wgpu::Buffer> = Vec::new();
1262 let mut uniform_bytes: Vec<u8> = Vec::new();
1263 for info in &shader.binding_infos {
1264 match info.kind {
1265 BindingKind::StorageRw | BindingKind::StorageRead => {
1266 if let Some(KernelParam::Buffer(buf)) = params.get(&info.name) {
1267 if let Some(wbuf) = buf.as_any().downcast_ref::<WebGPUBuffer>() {
1268 if let Some(ref inner) = wbuf.device_buffer {
1269 entries.push(wgpu::BindGroupEntry {
1270 binding: info.binding,
1271 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1272 buffer: inner,
1273 offset: 0,
1274 size: None,
1275 }),
1276 });
1277 }
1278 }
1279 } else {
1280 return Err(GpuError::InvalidParameter(format!(
1281 "Missing buffer param '{}'",
1282 info.name
1283 )));
1284 }
1285 }
1286 BindingKind::Uniform => {
1287 for (k, v) in params.iter() {
1289 if k == &info.name || k.starts_with(&(info.name.clone() + ".")) {
1290 match v {
1291 KernelParam::U32(u) => {
1292 uniform_bytes.extend_from_slice(&u.to_le_bytes())
1293 }
1294 KernelParam::I32(i) => {
1295 uniform_bytes.extend_from_slice(&i.to_le_bytes())
1296 }
1297 KernelParam::F32(f) => {
1298 uniform_bytes.extend_from_slice(&f.to_le_bytes())
1299 }
1300 KernelParam::F64(f) => {
1301 uniform_bytes.extend_from_slice(&f.to_le_bytes())
1302 }
1303 KernelParam::Bytes(b) => uniform_bytes.extend_from_slice(b),
1304 KernelParam::Buffer(_) => {}
1305 }
1306 }
1307 }
1308 }
1309 }
1310 }
1311 if !uniform_bytes.is_empty() {
1312 while uniform_bytes.len() % 16 != 0 {
1313 uniform_bytes.push(0);
1314 }
1315 if let Some(uinfo) = shader
1316 .binding_infos
1317 .iter()
1318 .find(|b| matches!(b.kind, BindingKind::Uniform))
1319 {
1320 if let Ok(mut list) = self.ephemeral_uniforms.lock() {
1321 list.clear();
1322 let ubuf = self
1323 .device
1324 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1325 label: Some("scirs2-uniforms"),
1326 contents: &uniform_bytes,
1327 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1328 });
1329 list.push(ubuf.clone());
1330 owned_uniform_buffers.push(ubuf.clone());
1331 let idx = owned_uniform_buffers.len() - 1;
1332 let buf_ref = &owned_uniform_buffers[idx];
1333 entries.push(wgpu::BindGroupEntry {
1334 binding: uinfo.binding,
1335 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1336 buffer: buf_ref,
1337 offset: 0,
1338 size: None,
1339 }),
1340 });
1341 }
1342 }
1343 } else if let Ok(mut list) = self.ephemeral_uniforms.lock() {
1344 list.clear();
1345 }
1346 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1347 label: Some("scirs2-bind-group"),
1348 layout: &shader.bind_group_layout,
1349 entries: &entries,
1350 });
1351 Ok(bind_group)
1352 }
1353}
1354
1355impl Drop for WebGPUBuffer {
1356 fn drop(&mut self) {
1357 if let Ok(mut pool) = self.memory_pool.lock() {
1359 #[cfg(feature = "wgpu")]
1360 {
1361 if let Some(buffer) = self.device_buffer.take() {
1363 pool.deallocate(buffer);
1364 }
1365 }
1366 #[cfg(not(feature = "wgpu"))]
1367 {
1368 if let Some(buffer) = self.device_buffer.take() {
1369 pool.deallocate(buffer);
1370 }
1371 }
1372 }
1373 }
1374}
1375
1376struct WebGPUCpuFallbackBuffer {
1379 data: Vec<u8>,
1380 size: usize,
1381 #[allow(dead_code)]
1382 memory_pool: Arc<Mutex<WebGPUMemoryPool>>,
1383}
1384
1385impl GpuBufferImpl for WebGPUCpuFallbackBuffer {
1386 fn size(&self) -> usize {
1387 self.size
1388 }
1389
1390 unsafe fn copy_from_host(&self, data: *const u8, size: usize) {
1391 if size > self.size {
1392 eprintln!("Warning: WebGPU CPU fallback buffer copy_from_host size mismatch");
1393 return;
1394 }
1395
1396 let data_slice = std::slice::from_raw_parts(data, size);
1398 eprintln!(
1401 "Warning: CPU fallback buffer copy_from_host called (size: {})",
1402 size
1403 );
1404 }
1405
1406 unsafe fn copy_to_host(&self, data: *mut u8, size: usize) {
1407 if size > self.size {
1408 eprintln!("Warning: WebGPU CPU fallback buffer copy_to_host size mismatch");
1409 return;
1410 }
1411
1412 let data_slice = std::slice::from_raw_parts_mut(data, size);
1414 let copy_size = size.min(self.data.len());
1415 data_slice[..copy_size].copy_from_slice(&self.data[..copy_size]);
1416
1417 eprintln!(
1418 "Warning: CPU fallback buffer copy_to_host called (size: {})",
1419 size
1420 );
1421 }
1422
1423 fn device_ptr(&self) -> u64 {
1424 self.data.as_ptr() as u64
1425 }
1426
1427 fn as_any(&self) -> &dyn std::any::Any {
1428 self
1429 }
1430}
1431
1432unsafe impl Send for WebGPUCpuFallbackBuffer {}
1434unsafe impl Sync for WebGPUCpuFallbackBuffer {}
1435
1436struct WebGPUMemoryPool {
1438 #[cfg(feature = "wgpu")]
1439 available_buffers: HashMap<usize, Vec<Buffer>>,
1440 #[cfg(not(feature = "wgpu"))]
1441 available_buffers: HashMap<usize, Vec<WgpuBuffer>>,
1442 #[allow(dead_code)]
1443 total_size: usize,
1444 used_size: usize,
1445}
1446
1447impl WebGPUMemoryPool {
1448 fn new(totalsize: usize) -> Self {
1449 Self {
1450 available_buffers: HashMap::new(),
1451 total_size: totalsize,
1452 used_size: 0,
1453 }
1454 }
1455
1456 #[cfg(feature = "wgpu")]
1457 fn allocate(&mut self, size: usize) -> Option<Buffer> {
1458 if let Some(buffers) = self.available_buffers.get_mut(&size) {
1460 if let Some(buffer) = buffers.pop() {
1461 self.used_size += size;
1462 return Some(buffer);
1463 }
1464 }
1465 None
1466 }
1467
1468 #[cfg(not(feature = "wgpu"))]
1469 fn allocate(&mut self, size: usize) -> Option<WgpuBuffer> {
1470 if let Some(buffers) = self.available_buffers.get_mut(&size) {
1472 if let Some(buffer) = buffers.pop() {
1473 self.used_size += size;
1474 return Some(buffer);
1475 }
1476 }
1477 None
1478 }
1479
1480 #[cfg(feature = "wgpu")]
1481 fn deallocate(&mut self, buffer: Buffer) {
1482 let size = buffer.size() as usize;
1484 self.available_buffers
1485 .entry(size)
1486 .or_insert_with(Vec::new)
1487 .push(buffer);
1488 self.used_size = self.used_size.saturating_sub(size);
1489 }
1490
1491 #[cfg(not(feature = "wgpu"))]
1492 fn deallocate(&mut self, buffer: WgpuBuffer) {
1493 let size = 1024; self.available_buffers
1496 .entry(size)
1497 .or_insert_with(Vec::new)
1498 .push(buffer);
1499 self.used_size = self.used_size.saturating_sub(size);
1500 }
1501
1502 #[allow(dead_code)]
1503 fn get_memory_usage(&self) -> (usize, usize) {
1504 (self.used_size, self.total_size)
1505 }
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510 use super::*;
1511 #[cfg(feature = "wgpu")]
1512 use std::any::TypeId;
1513
1514 #[cfg(not(feature = "wgpu"))]
1520 #[test]
1521 fn context_new_fails_honestly_without_wgpu_feature() {
1522 let result = WebGPUContext::new();
1523 assert!(
1524 result.is_err(),
1525 "WebGPUContext::new() must fail without the `wgpu` feature, not fabricate a context"
1526 );
1527 }
1528
1529 #[cfg(feature = "wgpu")]
1533 #[test]
1534 fn compile_typed_rejects_unknown_kernel_name() {
1535 if !WebGPUContext::is_available() {
1536 eprintln!("skipping: no WebGPU adapter available in this environment");
1537 return;
1538 }
1539 let context = WebGPUContext::new().expect("adapter reported available");
1540 let compiler = context.create_compiler();
1541
1542 let result = compiler.compile_typed(
1543 "totally_unknown_kernel",
1544 TypeId::of::<f32>(),
1545 TypeId::of::<f32>(),
1546 );
1547 assert!(
1548 result.is_err(),
1549 "an unrecognized kernel name must return an error, not a fabricated handle"
1550 );
1551 }
1552
1553 #[cfg(feature = "wgpu")]
1559 #[test]
1560 fn compile_typed_vector_add_computes_real_result_when_gpu_available() {
1561 if !WebGPUContext::is_available() {
1562 eprintln!("skipping: no WebGPU adapter available in this environment");
1563 return;
1564 }
1565
1566 let context = WebGPUContext::new().expect("adapter reported available");
1567 let compiler = context.create_compiler();
1568
1569 let a_data = [1.0f32, 2.0, 3.0, 4.0];
1570 let b_data = [10.0f32, 20.0, 30.0, 40.0];
1571 let byte_len = std::mem::size_of_val(&a_data);
1572
1573 let a_raw = context.create_buffer(byte_len);
1574 let b_raw = context.create_buffer(byte_len);
1575 let result_raw = context.create_buffer(byte_len);
1576
1577 let a = crate::gpu::GpuBuffer::<f32>::new(Arc::clone(&a_raw), a_data.len());
1578 let b = crate::gpu::GpuBuffer::<f32>::new(Arc::clone(&b_raw), b_data.len());
1579 let result = crate::gpu::GpuBuffer::<f32>::new(Arc::clone(&result_raw), a_data.len());
1580 a.copy_from_host(&a_data).expect("upload a");
1581 b.copy_from_host(&b_data).expect("upload b");
1582
1583 let kernel = compiler
1584 .compile_typed("vector_add", TypeId::of::<f32>(), TypeId::of::<f32>())
1585 .expect("real vector_add should compile with a live adapter");
1586 kernel.set_buffer("a", &a_raw);
1587 kernel.set_buffer("b", &b_raw);
1588 kernel.set_buffer("result", &result_raw);
1589 kernel.dispatch([1, 1, 1]);
1590
1591 let computed = result.to_vec();
1592 assert_eq!(
1593 computed,
1594 vec![11.0, 22.0, 33.0, 44.0],
1595 "vector_add must actually compute a + b on the GPU, not leave the output untouched"
1596 );
1597 }
1598}