Skip to main content

scirs2_core/gpu/backends/
wgpu.rs

1//! WebGPU backend implementation for GPU operations
2//!
3//! This module provides WebGPU-specific implementations for cross-platform GPU operations.
4
5use std::collections::HashMap;
6#[cfg(feature = "wgpu")]
7// wgpu 26 removed earlier Poll enum; Device::poll exists but Maintain enum not re-exported here; we avoid explicit polling for now.
8use 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// Fallback types for when WebGPU is not available
24#[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/// A compiled WebGPU compute pipeline, containing all state needed to dispatch a compute shader.
34///
35/// Created by [`try_compile_wgsl`]. On hosts without a GPU adapter this is never constructed
36/// and that function returns an error instead.
37#[cfg(feature = "wgpu")]
38pub struct WgpuComputePipeline {
39    /// The underlying wgpu compute pipeline.
40    pub pipeline: ComputePipeline,
41    /// The bind group layout derived from WGSL source inspection.
42    pub bind_group_layout: BindGroupLayout,
43    /// Workgroup size extracted from the `@workgroup_size(...)` attribute; defaults to `[64, 1, 1]`.
44    pub workgroup_size: [u32; 3],
45}
46
47#[cfg(feature = "wgpu")]
48// SAFETY: wgpu's `ComputePipeline` and `BindGroupLayout` are `Send + Sync` on all native backends.
49unsafe impl Send for WgpuComputePipeline {}
50#[cfg(feature = "wgpu")]
51unsafe impl Sync for WgpuComputePipeline {}
52
53/// Attempt to compile `source` as a WGSL compute shader and return a [`WgpuComputePipeline`].
54///
55/// # Errors
56/// - Returns an error if no wgpu adapter is available on the host (e.g. headless CI without GPU).
57/// - Returns an error if `source` contains invalid WGSL (wgpu panics on truly invalid WGSL;
58///   syntactically valid but semantically broken shaders will fail at pipeline creation).
59///
60/// # Example
61/// ```rust,no_run
62/// # #[cfg(feature = "wgpu")]
63/// # {
64/// use scirs2_core::gpu::backends::try_compile_wgsl;
65/// let pipeline = try_compile_wgsl(r#"
66///     @group(0) @binding(0) var<storage, read_write> out: array<f32>;
67///     @compute @workgroup_size(64)
68///     fn main(@builtin(global_invocation_id) gid: vec3<u32>) { out[gid.x] = f32(gid.x); }
69/// "#).expect("shader compiled");
70/// let _ = pipeline;
71/// # }
72/// ```
73#[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/// Run a vector-add compute shader end-to-end on the GPU.
80///
81/// Uploads `a` and `b` to device buffers, dispatches the WGSL kernel, then reads back the result.
82/// Returns `Ok(result_vec)` on success. Returns `Err` if no adapter is available.
83#[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
89// WebGPU shader source templates — used by the kernel registry in kernels/mod.rs
90// for the GEMM BLAS kernel (exposed here as a named constant for external use).
91
92/// WGSL source for the GEMM kernel (tiled 8×8 matrix multiply).
93///
94/// Computes C = alpha * A * B + beta * C where A is M×K, B is K×N, C is M×N.
95///
96/// Buffers: 0 → matrix_a (read), 1 → matrix_b (read), 2 → matrix_c (read_write)
97/// Uniforms: M, N, K, alpha, beta
98pub 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
130/// WGSL source for the `vector_add` kernel: `result = a + b`, elementwise.
131///
132/// Buffers: 0 → `a` (read), 1 → `b` (read), 2 → `result` (read_write).
133/// This is the one kernel [`WebGPUCompiler::compile_typed`] can compile for
134/// real by name (`f32` in, `f32` out); anything else must go through
135/// [`GpuCompiler::compile`](crate::gpu::GpuCompiler::compile) with real WGSL
136/// source, or [`GpuContext::get_kernel`](crate::gpu::GpuContext::get_kernel)
137/// for registry-backed named kernels.
138const 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
152/// WebGPU context wrapper
153pub 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
166// WebGPU handles are safe to send between threads when properly synchronized
167unsafe impl Send for WebGPUContext {}
168unsafe impl Sync for WebGPUContext {}
169
170impl WebGPUContext {
171    /// Create a new WebGPU context
172    pub fn new() -> Result<Self, GpuError> {
173        #[cfg(feature = "wgpu")]
174        {
175            // Real WebGPU implementation.
176            //
177            // Restrict to `Backends::PRIMARY` (Vulkan/Metal/DX12/BrowserWebGpu) rather than
178            // `Backends::all()`. The secondary GL/GLES backend can appear "compatible" on hosts
179            // with a broken or missing Vulkan loader (falling back to a surfaceless EGL context),
180            // but that GL context is frequently too limited for wgpu's mandatory internal
181            // indirect-dispatch validation pipeline (missing compute/storage-buffer features),
182            // so the device comes back already lost. Restricting to PRIMARY makes adapter
183            // discovery fail cleanly instead of handing back a device that dies on first use.
184            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                // Newer wgpu versions removed/changed some fields (e.g. trace Option). Use defaults for the rest.
205                ..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))), // 1GB pool
216            })
217        }
218        #[cfg(not(feature = "wgpu"))]
219        {
220            // The real `wgpu` crate is not compiled into this build: there is no
221            // adapter/device to create, so honestly report that rather than
222            // fabricating a context backed by placeholder pointer values (which
223            // would make every buffer/kernel operation on it a silent no-op).
224            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    /// Check if WebGPU is available and working
233    pub fn is_available() -> bool {
234        #[cfg(feature = "wgpu")]
235        {
236            // Real WebGPU implementation - try to create an instance and adapter.
237            // See the comment in `new()` for why this is restricted to `Backends::PRIMARY`.
238            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            // Try to get an adapter (this is async, so we use a simple runtime check)
248            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            // Fallback: return false since we don't have real WebGPU
262            false
263        }
264    }
265
266    /// Compile a shader from WGSL source
267    fn compile_shader_internal(&self, source: &str, name: &str) -> Result<WebGPUShader, GpuError> {
268        #[cfg(feature = "wgpu")]
269        {
270            // Real WebGPU implementation
271            let shader_module = self.device.create_shader_module(ShaderModuleDescriptor {
272                label: Some(name),
273                source: ShaderSource::Wgsl(source.into()),
274            });
275
276            // Extract entry point from source or use default
277            let entry_point = Self::extract_entry_point(source).unwrap_or("main");
278
279            // Create bind group layout + reflection infos
280            let (bind_group_layout, binding_infos) =
281                self.create_bind_group_layout_from_source(source, name)?;
282
283            // Create pipeline layout with our bind group layout
284            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                        // wgpu 28+: immediate_size replaces push_constant_ranges
290                        ..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            // Fallback implementation
317            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    /// Create bind group layout from WGSL source analysis
330    #[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                // variable declaration
372                if pending.group.unwrap_or(0) == 0 {
373                    // only group 0 for now
374                    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        // Deduplicate by binding number
443        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    /// Return a reference to the underlying `wgpu::Device`.
463    #[cfg(feature = "wgpu")]
464    pub fn device(&self) -> &Device {
465        &self.device
466    }
467
468    /// Return a reference to the underlying `wgpu::Queue`.
469    #[cfg(feature = "wgpu")]
470    pub fn queue(&self) -> &Queue {
471        &self.queue
472    }
473
474    /// Allocate device memory
475    #[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    // Fallback methods for when WebGPU (the `wgpu` feature) is not compiled in.
488    //
489    // `WebGPUContext::new()` always fails with `BackendNotAvailable` in this
490    // configuration (no adapter/device can exist), so `compile_shader_internal`
491    // below can never actually be reached through a live context — but it must
492    // still type-check for this cfg, and must not silently fabricate a
493    // "successful" compile if it ever were reached (e.g. via a future code
494    // path that bypasses `new()`).
495    #[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    /// Compile WGSL source into a [`WgpuComputePipeline`] (real-wgpu path only).
505    ///
506    /// This exposes the same compilation path as [`try_compile_wgsl`] but operates
507    /// on an already-created context so the adapter/device creation overhead is
508    /// incurred only once.
509    #[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    /// Run a vector-add end-to-end: upload `a` and `b`, dispatch, read back `result`.
520    #[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        // Compile shader
532        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        // Build bind group layout explicitly (3 storage bindings)
538        let bgl = self
539            .device
540            .create_bind_group_layout(&BindGroupLayoutDescriptor {
541                label: Some("vector-add-bgl"),
542                entries: &[
543                    // binding 0: a (read-only storage)
544                    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                    // binding 1: b (read-only storage)
555                    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                    // binding 2: result (read-write storage)
566                    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        // Upload input buffers
599        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        // Bind group
625        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        // Encode and dispatch
645        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        // Readback via staging buffer
662        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        // Poll until GPU work completes (required on native backends before map_async fires)
672        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        // Poll again to drive the map callback to completion
683        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    /// Extract the entry point function name from WGSL source code
703    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            // Check if this line contains @compute
710            if trimmed.contains("@compute") {
711                // The function might be on the same line or the next line
712                let mut search_line = trimmed;
713                let mut search_idx = 0;
714
715                // If @compute and function are not on the same line, check next line
716                if !search_line.contains("fn ") && search_idx + 1 < lines.len() {
717                    search_idx += 1;
718                    search_line = lines[search_idx].trim();
719                }
720
721                // Extract function name
722                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        // Try to allocate from memory pool first
739        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        // Fallback to direct allocation
756        let device_buffer = match self.allocate_device_memory(size) {
757            Ok(buffer) => buffer,
758            Err(e) => {
759                // Log the WebGPU allocation failure and create a CPU fallback
760                eprintln!(
761                    "Warning: WebGPU buffer allocation failed ({}), creating CPU fallback buffer",
762                    e
763                );
764
765                #[cfg(feature = "wgpu")]
766                {
767                    // Create a CPU fallback buffer with minimal size for WebGPU compatibility
768                    // This is a last resort when GPU memory is exhausted
769                    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
817/// WebGPU shader wrapper (augmented with basic reflection info)
818struct 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>, // basic reflection info (names may be synthetic when parser can't extract)
833    #[allow(dead_code)]
834    workgroup_size: [u32; 3],
835}
836
837// WebGPU shader handles are safe to send between threads when properly synchronized
838unsafe impl Send for WebGPUShader {}
839unsafe impl Sync for WebGPUShader {}
840
841/// WebGPU compiler implementation
842struct 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        // Actually register the compiled shader so `dispatch` can find it:
851        // previously this was compiled and then immediately dropped, so
852        // every kernel produced by `compile()` silently no-op'd on dispatch.
853        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        // Unlike `compile(source)`, this path is given only a *name* and a
885        // pair of types, not real shader source — it has no registry
886        // access to resolve arbitrary names (see `GpuContext::get_kernel`
887        // for that). The one case this can genuinely compile for real is
888        // the well-known `vector_add` kernel over `f32`; anything else
889        // honestly fails instead of handing back a handle that silently
890        // no-ops on every dispatch.
891        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
931/// WebGPU kernel handle for execution
932struct 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
976/// Extract the `@workgroup_size(x [, y [, z]])` values from WGSL source.
977/// Returns `[64, 1, 1]` as a sensible default if the attribute is not present or unparseable.
978fn 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    // raw bytes helper removed from trait; use internal helper if needed
1051
1052    fn dispatch(&self, workgroups: [u32; 3]) {
1053        #[cfg(feature = "wgpu")]
1054        {
1055            // Real WebGPU compute dispatch
1056            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                // Create command encoder
1067                let mut encoder =
1068                    self.device
1069                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1070                            label: Some("Compute Command Encoder"),
1071                        });
1072
1073                // Begin compute pass
1074                {
1075                    let mut compute_pass =
1076                        encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1077                            label: Some("Compute Pass"),
1078                            timestamp_writes: None,
1079                        });
1080
1081                    // Set the compute pipeline
1082                    compute_pass.set_pipeline(&shader.pipeline);
1083
1084                    if let Ok(bind_group) = self.create_bind_group_from_params(shader, &params) {
1085                        compute_pass.set_bind_group(0, &bind_group, &[]);
1086                    }
1087                    // else: bind group creation failed; dispatch will proceed but produce undefined results
1088
1089                    // Dispatch the compute shader
1090                    compute_pass.dispatch_workgroups(workgroups[0], workgroups[1], workgroups[2]);
1091                }
1092
1093                // Submit the command buffer
1094                let command_buffer = encoder.finish();
1095                self.queue.submit(std::iter::once(command_buffer));
1096            }
1097        }
1098        #[cfg(not(feature = "wgpu"))]
1099        {
1100            // Fallback: no GPU available; dispatch is a no-op
1101            let _ = workgroups;
1102            let _ = &self.shader_name;
1103        }
1104    }
1105}
1106
1107/// WebGPU buffer implementation
1108struct 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
1123// WebGPU buffer handles are safe to send between threads when properly synchronized
1124// The real wgpu types (Buffer, Queue) are Send + Sync
1125// For fallback types (raw pointers), we assume proper synchronization is handled externally
1126unsafe 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            // Validate data size
1138            if size > self.size {
1139                // In unsafe context, we can't return an error, so we'll just log and return
1140                eprintln!(
1141                    "Warning: Data size {} exceeds buffer size {}",
1142                    size, self.size
1143                );
1144                return;
1145            }
1146
1147            // Convert raw pointer to slice for WebGPU API
1148            let data_slice = std::slice::from_raw_parts(data, size);
1149
1150            // Real WebGPU implementation - write data to buffer
1151            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            // Fallback implementation - just validate
1158            if size > self.size {
1159                eprintln!(
1160                    "Warning: Data size {} exceeds buffer size {}",
1161                    size, self.size
1162                );
1163            }
1164            // In fallback mode, we just simulate the operation
1165        }
1166    }
1167
1168    unsafe fn copy_to_host(&self, data: *mut u8, size: usize) {
1169        #[cfg(feature = "wgpu")]
1170        {
1171            // Validate data size
1172            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                // Poll the device until all submitted work completes before mapping.
1196                // This is required on all native wgpu backends (Vulkan, Metal, DX12)
1197                // to ensure the copy completes before the slice can be mapped.
1198                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                // Drive map callback to completion
1206                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            // Fallback implementation - just validate and zero out
1221            if size > self.size {
1222                eprintln!(
1223                    "Warning: Data size {} exceeds buffer size {}",
1224                    size, self.size
1225                );
1226            }
1227
1228            // Zero out the data as a placeholder
1229            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            // WebGPU doesn't expose raw device pointers, so we return a placeholder
1238            // In a real implementation, this might return a handle or ID
1239            &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        // Hold uniform buffers so their lifetime extends until after bind_group creation
1261        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                    // Collect all scalars/bytes with key prefix or exact match
1288                    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        // Return buffer to memory pool if possible
1358        if let Ok(mut pool) = self.memory_pool.lock() {
1359            #[cfg(feature = "wgpu")]
1360            {
1361                // In real implementation, would return buffer to pool
1362                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
1376/// CPU fallback buffer for when WebGPU buffer allocation fails
1377/// This provides a graceful degradation when GPU memory is exhausted
1378struct 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        // Since this is a CPU fallback, we can use safe Rust internally
1397        let data_slice = std::slice::from_raw_parts(data, size);
1398        // We can't mutate self.data directly since &self is immutable
1399        // In a real implementation, this would require interior mutability
1400        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        // Copy from CPU buffer to host
1413        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
1432// Safety: WebGPUCpuFallbackBuffer is thread-safe since it only contains owned data
1433unsafe impl Send for WebGPUCpuFallbackBuffer {}
1434unsafe impl Sync for WebGPUCpuFallbackBuffer {}
1435
1436/// WebGPU memory pool for efficient buffer management
1437struct 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        // Try to find a suitable buffer in the pool
1459        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        // Try to find a suitable buffer in the pool
1471        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        // Return buffer to pool
1483        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        // Fallback implementation - track the buffer
1494        let size = 1024; // Placeholder size
1495        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    /// Without the real `wgpu` crate compiled in, there is no adapter or
1515    /// device to create: constructing a context must fail honestly rather
1516    /// than fabricating placeholder pointer handles (the previous
1517    /// behavior, under which every subsequent buffer/kernel operation
1518    /// silently did nothing).
1519    #[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    /// A kernel name this backend cannot generate real WGSL source for
1530    /// must fail loudly rather than silently succeeding with a handle that
1531    /// no-ops on every dispatch (the previous `compile_typed` behavior).
1532    #[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    /// Regression test: `compile_typed("vector_add", f32, f32)` must
1554    /// actually run real WGSL on the GPU and produce the correct sum, not
1555    /// silently no-op (previously the returned handle referenced a shader
1556    /// name that was never inserted into `compiled_shaders`, so dispatch
1557    /// found nothing and `result` stayed whatever it was initialized to).
1558    #[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}