Skip to main content

oxiphysics_gpu/compute/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::cell::RefCell;
6use std::collections::HashMap;
7
8/// Records a sequence of kernel dispatches for batched execution.
9///
10/// Compute passes accumulate dispatch commands and execute them in order.
11pub struct ComputePass {
12    /// Recorded dispatch commands: (kernel_name, work_size).
13    pub(super) commands: Vec<(String, usize)>,
14}
15impl ComputePass {
16    /// Create a new empty compute pass.
17    pub fn new() -> Self {
18        Self {
19            commands: Vec::new(),
20        }
21    }
22    /// Record a dispatch command.
23    pub fn dispatch(&mut self, kernel_name: &str, work_size: usize) {
24        self.commands.push((kernel_name.to_string(), work_size));
25    }
26    /// Return the number of recorded commands.
27    pub fn num_commands(&self) -> usize {
28        self.commands.len()
29    }
30    /// Return the recorded commands.
31    pub fn commands(&self) -> &[(String, usize)] {
32        &self.commands
33    }
34    /// Clear all recorded commands.
35    pub fn clear(&mut self) {
36        self.commands.clear();
37    }
38    /// Total work items across all recorded dispatches.
39    pub fn total_work_items(&self) -> usize {
40        self.commands.iter().map(|(_, ws)| ws).sum()
41    }
42}
43/// Describes how a buffer is used in a compute pass.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum BufferUsage {
46    /// Buffer is read-only (storage, read).
47    ReadOnly,
48    /// Buffer is write-only (storage, read_write but only written).
49    WriteOnly,
50    /// Buffer is read-write.
51    ReadWrite,
52    /// Buffer is a uniform (small, read-only parameters).
53    Uniform,
54}
55/// A single GPU command entry.
56#[derive(Debug, Clone)]
57pub enum GpuCommand {
58    /// Copy from one buffer to another.
59    CopyBuffer {
60        /// Source buffer identifier.
61        src: BufferId,
62        /// Destination buffer identifier.
63        dst: BufferId,
64        /// Number of bytes to copy.
65        size: usize,
66    },
67    /// Dispatch a compute kernel.
68    DispatchCompute {
69        /// Name of the compute kernel.
70        kernel_name: String,
71        /// Workgroup counts for each dimension.
72        workgroups: [u32; 3],
73    },
74    /// Insert a pipeline barrier.
75    Barrier(PipelineBarrier),
76    /// Set a push constant value.
77    PushConstant {
78        /// Push constant name.
79        name: String,
80        /// Push constant value.
81        value: f64,
82    },
83}
84/// Tracks buffer lifecycle (creation, writes, reads) for debugging.
85pub struct ResourceLifecycle {
86    pub(super) events: Vec<ResourceEvent>,
87}
88impl ResourceLifecycle {
89    /// Create a new lifecycle tracker.
90    pub fn new() -> Self {
91        Self { events: Vec::new() }
92    }
93    /// Record a creation event.
94    pub fn record_create(&mut self, id: BufferId, size: usize) {
95        self.events.push(ResourceEvent::Created(id, size));
96    }
97    /// Record a write event.
98    pub fn record_write(&mut self, id: BufferId) {
99        self.events.push(ResourceEvent::Written(id));
100    }
101    /// Record a read event.
102    pub fn record_read(&mut self, id: BufferId) {
103        self.events.push(ResourceEvent::Read(id));
104    }
105    /// Record a destroy event.
106    pub fn record_destroy(&mut self, id: BufferId) {
107        self.events.push(ResourceEvent::Destroyed(id));
108    }
109    /// Return all events.
110    pub fn events(&self) -> &[ResourceEvent] {
111        &self.events
112    }
113    /// Return the number of events recorded.
114    pub fn len(&self) -> usize {
115        self.events.len()
116    }
117    /// Check if no events have been recorded.
118    pub fn is_empty(&self) -> bool {
119        self.events.is_empty()
120    }
121    /// Clear all events.
122    pub fn clear(&mut self) {
123        self.events.clear();
124    }
125    /// Count events of a specific type for a given buffer.
126    pub fn count_writes(&self, id: BufferId) -> usize {
127        self.events
128            .iter()
129            .filter(|e| matches!(e, ResourceEvent::Written(bid) if * bid == id))
130            .count()
131    }
132    /// Count reads for a given buffer.
133    pub fn count_reads(&self, id: BufferId) -> usize {
134        self.events
135            .iter()
136            .filter(|e| matches!(e, ResourceEvent::Read(bid) if * bid == id))
137            .count()
138    }
139}
140/// Specifies the type of pipeline barrier needed between passes.
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum PipelineBarrier {
143    /// Ensure all writes to storage buffers are visible before reading.
144    StorageReadAfterWrite,
145    /// Ensure all writes to uniform buffers are visible.
146    UniformReadAfterWrite,
147    /// Full barrier (all types).
148    Full,
149    /// No barrier needed.
150    None,
151}
152/// GPU occupancy model (simplified).
153///
154/// Models occupancy as the ratio of active warps to maximum concurrent warps.
155#[derive(Debug, Clone)]
156pub struct OccupancyModel {
157    /// Total number of compute units (SMs / CUs).
158    pub compute_units: u32,
159    /// Maximum warps per compute unit.
160    pub max_warps_per_cu: u32,
161    /// Warp size (threads per warp, typically 32 for NVIDIA or 64 for AMD).
162    pub warp_size: u32,
163    /// Shared memory per compute unit (bytes).
164    pub shared_mem_per_cu: u32,
165    /// Registers per compute unit.
166    pub registers_per_cu: u32,
167}
168impl OccupancyModel {
169    /// Create a model resembling a mid-range discrete GPU.
170    pub fn mid_range() -> Self {
171        Self {
172            compute_units: 32,
173            max_warps_per_cu: 32,
174            warp_size: 32,
175            shared_mem_per_cu: 48 * 1024,
176            registers_per_cu: 65536,
177        }
178    }
179    /// Estimate the theoretical occupancy (0.0–1.0) for a kernel.
180    ///
181    /// Occupancy is limited by:
182    /// 1. Workgroup size (must not exceed warp_size * max_warps_per_cu).
183    /// 2. Shared memory usage.
184    /// 3. Register usage.
185    pub fn estimate_occupancy(
186        &self,
187        workgroup_size: u32,
188        shared_mem_bytes: u32,
189        registers_per_thread: u32,
190    ) -> f64 {
191        let warps_per_wg = workgroup_size.div_ceil(self.warp_size);
192        let max_wg_by_warps = self.max_warps_per_cu / warps_per_wg.max(1);
193        let max_wg_by_smem = self
194            .shared_mem_per_cu
195            .checked_div(shared_mem_bytes)
196            .unwrap_or(u32::MAX);
197        let regs_per_wg = registers_per_thread * workgroup_size;
198        let max_wg_by_regs = self
199            .registers_per_cu
200            .checked_div(regs_per_wg)
201            .unwrap_or(u32::MAX);
202        let active_wg = max_wg_by_warps.min(max_wg_by_smem).min(max_wg_by_regs);
203        let active_warps = (active_wg * warps_per_wg).min(self.max_warps_per_cu);
204        (active_warps as f64 / self.max_warps_per_cu as f64).clamp(0.0, 1.0)
205    }
206    /// Total theoretical peak throughput in GFLOP/s (mock model).
207    ///
208    /// Assumes 2 FP32 ops per clock per SIMD unit.
209    pub fn peak_gflops(&self, clock_mhz: f64) -> f64 {
210        let simd_width = self.warp_size as f64;
211        2.0 * simd_width * self.compute_units as f64 * clock_mhz * 1e6 / 1e9
212    }
213}
214/// A recorded sequence of GPU commands (mock encoder).
215///
216/// Records commands for later submission; models wgpu-style recording.
217pub struct GpuCommandEncoder {
218    pub(super) label: String,
219    pub(super) commands: Vec<GpuCommand>,
220}
221impl GpuCommandEncoder {
222    /// Create a new command encoder with a debug label.
223    pub fn new(label: impl Into<String>) -> Self {
224        Self {
225            label: label.into(),
226            commands: Vec::new(),
227        }
228    }
229    /// Record a buffer copy command.
230    pub fn copy_buffer(&mut self, src: BufferId, dst: BufferId, size: usize) {
231        self.commands
232            .push(GpuCommand::CopyBuffer { src, dst, size });
233    }
234    /// Record a compute dispatch.
235    pub fn dispatch_compute(&mut self, kernel_name: &str, workgroups: [u32; 3]) {
236        self.commands.push(GpuCommand::DispatchCompute {
237            kernel_name: kernel_name.to_string(),
238            workgroups,
239        });
240    }
241    /// Insert a pipeline barrier.
242    pub fn insert_barrier(&mut self, barrier: PipelineBarrier) {
243        self.commands.push(GpuCommand::Barrier(barrier));
244    }
245    /// Set a named push constant.
246    pub fn push_constant(&mut self, name: &str, value: f64) {
247        self.commands.push(GpuCommand::PushConstant {
248            name: name.to_string(),
249            value,
250        });
251    }
252    /// Return the label of this encoder.
253    pub fn label(&self) -> &str {
254        &self.label
255    }
256    /// Number of recorded commands.
257    pub fn command_count(&self) -> usize {
258        self.commands.len()
259    }
260    /// Return the recorded commands.
261    pub fn commands(&self) -> &[GpuCommand] {
262        &self.commands
263    }
264    /// Reset the encoder (clear recorded commands).
265    pub fn reset(&mut self) {
266        self.commands.clear();
267    }
268    /// "Submit" the recorded commands: replay them on the dispatcher.
269    ///
270    /// For copy commands, data is transferred between buffers.
271    /// Other commands are noted but not executed (they are mock-only).
272    pub fn submit(&self, dispatcher: &mut ComputeDispatcher) -> Result<(), GpuError> {
273        for cmd in &self.commands {
274            if let GpuCommand::CopyBuffer { src, dst, .. } = cmd {
275                dispatcher.copy_buffer(*src, *dst)?;
276            }
277        }
278        Ok(())
279    }
280}
281/// Manages GPU buffers and dispatches parallel map/reduce operations.
282///
283/// This is a CPU-side simulation of GPU compute dispatch that uses a thread
284/// pool metaphor (sequential execution) for testing without a real GPU.
285pub struct ComputeDispatcher {
286    pub(super) buffers: HashMap<BufferId, GpuBuffer>,
287    pub(super) next_id: u32,
288}
289impl ComputeDispatcher {
290    /// Create a new dispatcher with no buffers.
291    pub fn new() -> Self {
292        Self {
293            buffers: HashMap::new(),
294            next_id: 0,
295        }
296    }
297    /// Allocate a new buffer of `size` f64 elements, optionally pre-loaded
298    /// with `initial_data`.  Returns the new buffer's [`BufferId`].
299    pub fn create_buffer(&mut self, size: usize, initial_data: Option<&[f64]>) -> BufferId {
300        let id = BufferId(self.next_id);
301        self.next_id += 1;
302        let buf = match initial_data {
303            Some(data) => {
304                let mut b = GpuBuffer::new(size);
305                let copy_len = data.len().min(size);
306                b.data[..copy_len].copy_from_slice(&data[..copy_len]);
307                b
308            }
309            None => GpuBuffer::new(size),
310        };
311        self.buffers.insert(id, buf);
312        id
313    }
314    /// Write `data` into the buffer identified by `id`.
315    ///
316    /// # Errors
317    /// Returns [`GpuError::InvalidBuffer`] if `id` is not registered.
318    pub fn write_buffer(&mut self, id: BufferId, data: &[f64]) -> Result<(), GpuError> {
319        match self.buffers.get_mut(&id) {
320            Some(buf) => {
321                buf.data = data.to_vec();
322                buf.size = data.len();
323                Ok(())
324            }
325            None => Err(GpuError::InvalidBuffer(id)),
326        }
327    }
328    /// Read the contents of the buffer identified by `id`.
329    ///
330    /// # Errors
331    /// Returns [`GpuError::InvalidBuffer`] if `id` is not registered.
332    pub fn read_buffer(&self, id: BufferId) -> Result<Vec<f64>, GpuError> {
333        self.buffers
334            .get(&id)
335            .map(|b| b.data.clone())
336            .ok_or(GpuError::InvalidBuffer(id))
337    }
338    /// Return the number of buffers currently managed.
339    pub fn num_buffers(&self) -> usize {
340        self.buffers.len()
341    }
342    /// Check if a buffer exists.
343    pub fn has_buffer(&self, id: BufferId) -> bool {
344        self.buffers.contains_key(&id)
345    }
346    /// Return the size of a buffer.
347    pub fn buffer_size(&self, id: BufferId) -> Result<usize, GpuError> {
348        self.buffers
349            .get(&id)
350            .map(|b| b.size)
351            .ok_or(GpuError::InvalidBuffer(id))
352    }
353    /// Destroy (remove) a buffer.
354    pub fn destroy_buffer(&mut self, id: BufferId) -> Result<(), GpuError> {
355        self.buffers
356            .remove(&id)
357            .map(|_| ())
358            .ok_or(GpuError::InvalidBuffer(id))
359    }
360    /// Copy data from one buffer to another.
361    pub fn copy_buffer(&mut self, src: BufferId, dst: BufferId) -> Result<(), GpuError> {
362        let src_data = self
363            .buffers
364            .get(&src)
365            .ok_or(GpuError::InvalidBuffer(src))?
366            .data
367            .clone();
368        let dst_buf = self
369            .buffers
370            .get_mut(&dst)
371            .ok_or(GpuError::InvalidBuffer(dst))?;
372        if src_data.len() != dst_buf.size {
373            return Err(GpuError::SizeMismatch {
374                expected: dst_buf.size,
375                got: src_data.len(),
376            });
377        }
378        dst_buf.data = src_data;
379        Ok(())
380    }
381    /// Dispatch a parallel map: `out[i] = f(in[i])` for every element.
382    ///
383    /// # Errors
384    /// Returns an error if either buffer is invalid or sizes differ.
385    pub fn dispatch_map(
386        &mut self,
387        buf_in: BufferId,
388        buf_out: BufferId,
389        f: impl Fn(f64) -> f64,
390    ) -> Result<(), GpuError> {
391        let input = self
392            .buffers
393            .get(&buf_in)
394            .ok_or(GpuError::InvalidBuffer(buf_in))?
395            .data
396            .clone();
397        let out_buf = self
398            .buffers
399            .get_mut(&buf_out)
400            .ok_or(GpuError::InvalidBuffer(buf_out))?;
401        if input.len() != out_buf.size {
402            return Err(GpuError::SizeMismatch {
403                expected: out_buf.size,
404                got: input.len(),
405            });
406        }
407        out_buf.data = input.iter().map(|&x| f(x)).collect();
408        Ok(())
409    }
410    /// Dispatch a parallel map with index: `out[i] = f(i, in[i])`.
411    pub fn dispatch_map_indexed(
412        &mut self,
413        buf_in: BufferId,
414        buf_out: BufferId,
415        f: impl Fn(usize, f64) -> f64,
416    ) -> Result<(), GpuError> {
417        let input = self
418            .buffers
419            .get(&buf_in)
420            .ok_or(GpuError::InvalidBuffer(buf_in))?
421            .data
422            .clone();
423        let out_buf = self
424            .buffers
425            .get_mut(&buf_out)
426            .ok_or(GpuError::InvalidBuffer(buf_out))?;
427        if input.len() != out_buf.size {
428            return Err(GpuError::SizeMismatch {
429                expected: out_buf.size,
430                got: input.len(),
431            });
432        }
433        out_buf.data = input.iter().enumerate().map(|(i, &x)| f(i, x)).collect();
434        Ok(())
435    }
436    /// Dispatch a zip-map: `out[i] = f(a[i], b[i])`.
437    pub fn dispatch_zip_map(
438        &mut self,
439        buf_a: BufferId,
440        buf_b: BufferId,
441        buf_out: BufferId,
442        f: impl Fn(f64, f64) -> f64,
443    ) -> Result<(), GpuError> {
444        let a_data = self
445            .buffers
446            .get(&buf_a)
447            .ok_or(GpuError::InvalidBuffer(buf_a))?
448            .data
449            .clone();
450        let b_data = self
451            .buffers
452            .get(&buf_b)
453            .ok_or(GpuError::InvalidBuffer(buf_b))?
454            .data
455            .clone();
456        if a_data.len() != b_data.len() {
457            return Err(GpuError::SizeMismatch {
458                expected: a_data.len(),
459                got: b_data.len(),
460            });
461        }
462        let out_buf = self
463            .buffers
464            .get_mut(&buf_out)
465            .ok_or(GpuError::InvalidBuffer(buf_out))?;
466        if a_data.len() != out_buf.size {
467            return Err(GpuError::SizeMismatch {
468                expected: out_buf.size,
469                got: a_data.len(),
470            });
471        }
472        out_buf.data = a_data
473            .iter()
474            .zip(b_data.iter())
475            .map(|(&a, &b)| f(a, b))
476            .collect();
477        Ok(())
478    }
479    /// Dispatch a parallel reduce: folds all elements in `buf` using `f`.
480    ///
481    /// Mimics a GPU tree-reduction (sequential here for correctness).
482    ///
483    /// # Errors
484    /// Returns [`GpuError::InvalidBuffer`] or [`GpuError::EmptyBuffer`].
485    pub fn dispatch_reduce(
486        &self,
487        buf: BufferId,
488        f: impl Fn(f64, f64) -> f64,
489    ) -> Result<f64, GpuError> {
490        let data = self.buffers.get(&buf).ok_or(GpuError::InvalidBuffer(buf))?;
491        let mut iter = data.data.iter().copied();
492        let first = iter.next().ok_or(GpuError::EmptyBuffer)?;
493        Ok(iter.fold(first, f))
494    }
495    /// Dispatch a mock SPH density kernel.
496    ///
497    /// Computes a simplified SPH density estimate for each particle:
498    ///
499    /// ```text
500    /// rho_i = sum_j m_j * W(|r_i - r_j|, h)
501    /// ```
502    ///
503    /// where `W(r, h) = max(0, 1 - (r/h)^2)` (simplified poly-6 mock).
504    ///
505    /// Buffer layout (flat f64):
506    /// * `pos_buf` — `[x0, y0, z0, x1, y1, z1, ...]`
507    /// * `mass_buf` — `[m0, m1, ...]`
508    /// * `out_density_buf` — written with `[rho0, rho1, ...]`
509    ///
510    /// # Errors
511    /// Returns an error if any buffer id is invalid.
512    pub fn dispatch_sph_density(
513        &mut self,
514        pos_buf: BufferId,
515        mass_buf: BufferId,
516        h: f64,
517        out_density_buf: BufferId,
518    ) -> Result<(), GpuError> {
519        let positions = self
520            .buffers
521            .get(&pos_buf)
522            .ok_or(GpuError::InvalidBuffer(pos_buf))?
523            .data
524            .clone();
525        let masses = self
526            .buffers
527            .get(&mass_buf)
528            .ok_or(GpuError::InvalidBuffer(mass_buf))?
529            .data
530            .clone();
531        let n = positions.len() / 3;
532        let h2 = h * h;
533        let mut densities = vec![0.0f64; n];
534        for i in 0..n {
535            let xi = positions[i * 3];
536            let yi = positions[i * 3 + 1];
537            let zi = positions[i * 3 + 2];
538            let mut rho = 0.0;
539            for j in 0..n {
540                let dx = xi - positions[j * 3];
541                let dy = yi - positions[j * 3 + 1];
542                let dz = zi - positions[j * 3 + 2];
543                let r2 = dx * dx + dy * dy + dz * dz;
544                if r2 < h2 {
545                    let q = 1.0 - r2 / h2;
546                    rho += masses[j] * q * q;
547                }
548            }
549            densities[i] = rho;
550        }
551        let out_buf = self
552            .buffers
553            .get_mut(&out_density_buf)
554            .ok_or(GpuError::InvalidBuffer(out_density_buf))?;
555        out_buf.data = densities;
556        out_buf.size = n;
557        Ok(())
558    }
559    /// Dispatch a tree-based parallel reduction on a buffer.
560    ///
561    /// Simulates a GPU tree reduction: repeatedly halves the active range,
562    /// summing adjacent elements until one value remains.
563    ///
564    /// Returns the reduced value (identity `0.0` for an empty buffer).
565    pub fn dispatch_reduction_tree(&self, buf: BufferId) -> Result<f64, GpuError> {
566        let data = self
567            .buffers
568            .get(&buf)
569            .ok_or(GpuError::InvalidBuffer(buf))?
570            .data
571            .clone();
572        if data.is_empty() {
573            return Ok(0.0);
574        }
575        let mut work = data;
576        let mut len = work.len();
577        while len > 1 {
578            let half = len / 2;
579            for i in 0..half {
580                work[i] = work[i * 2] + work[i * 2 + 1];
581            }
582            if len % 2 == 1 {
583                work[half] = work[len - 1];
584                len = half + 1;
585            } else {
586                len = half;
587            }
588        }
589        Ok(work[0])
590    }
591    /// Dispatch an inclusive prefix scan (cumulative sum) on a buffer.
592    ///
593    /// Writes `out[i] = sum(in[0..=i])` into `out_buf`.
594    ///
595    /// Uses a sequential Hillis-Steele-style scan for correctness.
596    pub fn dispatch_inclusive_scan(
597        &mut self,
598        buf_in: BufferId,
599        buf_out: BufferId,
600    ) -> Result<(), GpuError> {
601        let data = self
602            .buffers
603            .get(&buf_in)
604            .ok_or(GpuError::InvalidBuffer(buf_in))?
605            .data
606            .clone();
607        let n = data.len();
608        let mut result = data;
609        for i in 1..n {
610            result[i] += result[i - 1];
611        }
612        let out = self
613            .buffers
614            .get_mut(&buf_out)
615            .ok_or(GpuError::InvalidBuffer(buf_out))?;
616        out.data = result;
617        out.size = n;
618        Ok(())
619    }
620    /// Dispatch a 2-bit radix sort on a buffer of non-negative f64 values.
621    ///
622    /// Values are cast to u64 (bit-cast) and sorted by their bits using
623    /// counting sort passes with 2-bit digits.  32 passes cover all 64 bits.
624    /// For non-negative IEEE 754 doubles the bit pattern order matches numeric
625    /// order.  Returns the sorted data as a new `Vec`f64` (input unchanged).
626    pub fn dispatch_radix_sort(&self, buf: BufferId) -> Result<Vec<f64>, GpuError> {
627        let data = self
628            .buffers
629            .get(&buf)
630            .ok_or(GpuError::InvalidBuffer(buf))?
631            .data
632            .clone();
633        let n = data.len();
634        if n == 0 {
635            return Ok(Vec::new());
636        }
637        let mut keys: Vec<u64> = data.iter().map(|&v| v.to_bits()).collect();
638        for pass in 0..32usize {
639            let shift = pass * 2;
640            let mut counts = [0usize; 4];
641            for &k in &keys {
642                counts[((k >> shift) & 0x3) as usize] += 1;
643            }
644            let mut starts = [0usize; 4];
645            for i in 1..4 {
646                starts[i] = starts[i - 1] + counts[i - 1];
647            }
648            let mut out = vec![0u64; n];
649            let mut pos = starts;
650            for &k in &keys {
651                let digit = ((k >> shift) & 0x3) as usize;
652                out[pos[digit]] = k;
653                pos[digit] += 1;
654            }
655            keys = out;
656        }
657        Ok(keys.iter().map(|&bits| f64::from_bits(bits)).collect())
658    }
659}
660/// Opaque handle to a GPU/CPU buffer (usize-indexed, used by ComputeBackend).
661#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
662pub struct BufferHandle(pub usize);
663/// Specification for a GPU compute kernel dispatch.
664#[derive(Debug, Clone)]
665pub struct KernelSpec {
666    /// Human-readable kernel name.
667    pub name: String,
668    /// Number of threads per workgroup `\[X, Y, Z\]`.
669    pub workgroup_size: [u32; 3],
670    /// Ordered list of buffer bindings for this kernel.
671    pub buffer_bindings: Vec<BufferId>,
672}
673impl KernelSpec {
674    /// Create a new kernel spec with a 1-D workgroup.
675    pub fn new(name: impl Into<String>, workgroup_x: u32, buffer_bindings: Vec<BufferId>) -> Self {
676        Self {
677            name: name.into(),
678            workgroup_size: [workgroup_x, 1, 1],
679            buffer_bindings,
680        }
681    }
682    /// Create a kernel spec with a 3-D workgroup size.
683    pub fn with_workgroup_3d(
684        name: impl Into<String>,
685        workgroup_size: [u32; 3],
686        buffer_bindings: Vec<BufferId>,
687    ) -> Self {
688        Self {
689            name: name.into(),
690            workgroup_size,
691            buffer_bindings,
692        }
693    }
694    /// Compute the number of workgroups needed for `total_items` in the X dimension.
695    pub fn num_workgroups_x(&self, total_items: u32) -> u32 {
696        total_items.div_ceil(self.workgroup_size[0])
697    }
698    /// Total threads per workgroup.
699    pub fn threads_per_workgroup(&self) -> u32 {
700        self.workgroup_size[0] * self.workgroup_size[1] * self.workgroup_size[2]
701    }
702}
703/// A CPU-resident buffer that mimics a GPU storage buffer.
704#[derive(Debug, Clone)]
705pub struct GpuBuffer {
706    /// Buffer contents as f64.
707    pub data: Vec<f64>,
708    /// Declared capacity of the buffer (may differ from `data.len()` if
709    /// the buffer was created with a fixed size but partially written).
710    pub size: usize,
711}
712impl GpuBuffer {
713    /// Create a zero-filled buffer of `size` elements.
714    pub fn new(size: usize) -> Self {
715        Self {
716            data: vec![0.0; size],
717            size,
718        }
719    }
720    /// Create a buffer pre-loaded with `initial_data`.
721    pub fn from_data(initial_data: Vec<f64>) -> Self {
722        let size = initial_data.len();
723        Self {
724            data: initial_data,
725            size,
726        }
727    }
728    /// Fill the buffer with a constant value.
729    pub fn fill(&mut self, value: f64) {
730        for v in &mut self.data {
731            *v = value;
732        }
733    }
734    /// Clear the buffer (set all elements to 0).
735    pub fn clear(&mut self) {
736        self.fill(0.0);
737    }
738    /// Get a slice of the buffer data.
739    pub fn as_slice(&self) -> &[f64] {
740        &self.data
741    }
742    /// Get a mutable slice of the buffer data.
743    pub fn as_mut_slice(&mut self) -> &mut [f64] {
744        &mut self.data
745    }
746    /// Number of bytes the buffer would occupy on GPU (f64 = 8 bytes each).
747    pub fn byte_size(&self) -> usize {
748        self.size * std::mem::size_of::<f64>()
749    }
750}
751/// Errors that can occur during GPU (or CPU-fallback) dispatch.
752#[derive(Debug, Clone, PartialEq)]
753pub enum GpuError {
754    /// The specified buffer id is not registered.
755    InvalidBuffer(BufferId),
756    /// Input and output buffers must have the same length.
757    SizeMismatch {
758        /// Expected buffer size.
759        expected: usize,
760        /// Actual buffer size received.
761        got: usize,
762    },
763    /// The reduction was attempted on an empty buffer.
764    EmptyBuffer,
765    /// A kernel or operation was not found.
766    NotFound(String),
767}
768/// CPU fallback compute backend.
769///
770/// Stores buffers as `Vec`f64` in memory and dispatches kernels on the CPU.
771pub struct CpuBackend {
772    pub(super) buffers: RefCell<Vec<Vec<f64>>>,
773}
774impl CpuBackend {
775    /// Create a new CPU backend.
776    pub fn new() -> Self {
777        Self {
778            buffers: RefCell::new(Vec::new()),
779        }
780    }
781    /// Return the number of buffers currently allocated.
782    pub fn num_buffers(&self) -> usize {
783        self.buffers.borrow().len()
784    }
785    /// Return the total number of f64 elements across all buffers.
786    pub fn total_elements(&self) -> usize {
787        self.buffers.borrow().iter().map(|b| b.len()).sum()
788    }
789}
790/// A single resource event for lifecycle tracking.
791#[derive(Debug, Clone)]
792pub enum ResourceEvent {
793    /// Buffer was created.
794    Created(BufferId, usize),
795    /// Buffer was written to.
796    Written(BufferId),
797    /// Buffer was read from.
798    Read(BufferId),
799    /// Buffer was destroyed.
800    Destroyed(BufferId),
801}
802/// A record of divergent branches observed in a kernel.
803#[derive(Debug, Clone, Default)]
804pub struct WarpDivergenceRecord {
805    /// Number of branch instructions encountered.
806    pub total_branches: u64,
807    /// Number of branches where threads diverged (not all took same path).
808    pub divergent_branches: u64,
809}
810impl WarpDivergenceRecord {
811    /// Compute the divergence rate (0.0 = no divergence, 1.0 = fully divergent).
812    pub fn divergence_rate(&self) -> f64 {
813        if self.total_branches == 0 {
814            0.0
815        } else {
816            self.divergent_branches as f64 / self.total_branches as f64
817        }
818    }
819    /// Estimated performance penalty from divergence (relative slowdown factor).
820    ///
821    /// A simple model: penalty = 1 + divergence_rate * (warp_size - 1) / warp_size.
822    pub fn performance_penalty(&self, warp_size: u32) -> f64 {
823        let rate = self.divergence_rate();
824        1.0 + rate * (warp_size as f64 - 1.0) / warp_size as f64
825    }
826}
827/// A mock GPU timeline semaphore for synchronising multi-pass GPU work.
828///
829/// On real GPU APIs (Vulkan, D3D12), timeline semaphores allow the CPU to
830/// wait for a specific GPU progress point.  This mock records signal and
831/// wait operations for testing.
832pub struct TimelineSemaphore {
833    /// Current value of the semaphore counter.
834    pub value: u64,
835    /// History of signalled values.
836    pub(super) signal_history: Vec<u64>,
837    /// History of wait requests.
838    pub(super) wait_history: Vec<u64>,
839}
840impl TimelineSemaphore {
841    /// Create a new semaphore starting at value 0.
842    pub fn new() -> Self {
843        Self {
844            value: 0,
845            signal_history: Vec::new(),
846            wait_history: Vec::new(),
847        }
848    }
849    /// Signal the semaphore to `new_value`.  Value must be monotonically increasing.
850    pub fn signal(&mut self, new_value: u64) {
851        assert!(
852            new_value > self.value,
853            "semaphore values must increase monotonically"
854        );
855        self.value = new_value;
856        self.signal_history.push(new_value);
857    }
858    /// Record a wait-for request.  In a mock, this checks `wait_value <= current`.
859    ///
860    /// Returns `true` if the semaphore has already reached `wait_value`.
861    pub fn wait(&mut self, wait_value: u64) -> bool {
862        self.wait_history.push(wait_value);
863        self.value >= wait_value
864    }
865    /// Return the current semaphore value.
866    pub fn current_value(&self) -> u64 {
867        self.value
868    }
869    /// Number of times the semaphore has been signalled.
870    pub fn signal_count(&self) -> usize {
871        self.signal_history.len()
872    }
873}
874/// GPU memory bandwidth model.
875///
876/// Estimates the effective bandwidth and the roofline-model bound for a kernel.
877#[derive(Debug, Clone)]
878pub struct MemoryBandwidthModel {
879    /// Peak memory bandwidth in GB/s.
880    pub peak_bandwidth_gbs: f64,
881    /// Peak compute throughput in GFLOP/s.
882    pub peak_compute_gflops: f64,
883}
884impl MemoryBandwidthModel {
885    /// Create a model for a mid-range discrete GPU.
886    pub fn mid_range() -> Self {
887        Self {
888            peak_bandwidth_gbs: 480.0,
889            peak_compute_gflops: 10000.0,
890        }
891    }
892    /// Compute the arithmetic intensity (FLOP/byte) of a kernel.
893    ///
894    /// `flops` – total floating-point operations.
895    /// `bytes_accessed` – total bytes read/written.
896    pub fn arithmetic_intensity(flops: f64, bytes_accessed: f64) -> f64 {
897        if bytes_accessed < 1e-30 {
898            f64::INFINITY
899        } else {
900            flops / bytes_accessed
901        }
902    }
903    /// Roofline performance estimate (GFLOP/s) given arithmetic intensity.
904    pub fn roofline_performance(&self, arithmetic_intensity: f64) -> f64 {
905        let bw_bound = arithmetic_intensity * self.peak_bandwidth_gbs;
906        bw_bound.min(self.peak_compute_gflops)
907    }
908    /// Estimated kernel execution time in milliseconds.
909    ///
910    /// `flops` – total FLOPs in the kernel.
911    /// `bytes_accessed` – total bytes transferred.
912    pub fn estimated_runtime_ms(&self, flops: f64, bytes_accessed: f64) -> f64 {
913        let intensity = Self::arithmetic_intensity(flops, bytes_accessed);
914        let perf_gflops = self.roofline_performance(intensity);
915        if perf_gflops < 1e-30 {
916            return f64::INFINITY;
917        }
918        (flops / (perf_gflops * 1e9)) * 1e3
919    }
920    /// Whether the kernel is bandwidth-bound or compute-bound.
921    ///
922    /// Returns `true` if bandwidth-bound (arithmetic intensity below the ridge point).
923    pub fn is_bandwidth_bound(&self, arithmetic_intensity: f64) -> bool {
924        let ridge_point = self.peak_compute_gflops / self.peak_bandwidth_gbs;
925        arithmetic_intensity < ridge_point
926    }
927}
928/// A binding entry associating a buffer with a binding index and usage.
929#[derive(Debug, Clone, Copy)]
930pub struct BufferBinding {
931    /// Binding index in the shader (e.g. @binding(0)).
932    pub binding: u32,
933    /// The buffer to bind.
934    pub buffer_id: BufferId,
935    /// Usage of the buffer in this binding.
936    pub usage: BufferUsage,
937}
938impl BufferBinding {
939    /// Create a new buffer binding.
940    pub fn new(binding: u32, buffer_id: BufferId, usage: BufferUsage) -> Self {
941        Self {
942            binding,
943            buffer_id,
944            usage,
945        }
946    }
947    /// Shorthand for a read-only binding.
948    pub fn read(binding: u32, buffer_id: BufferId) -> Self {
949        Self::new(binding, buffer_id, BufferUsage::ReadOnly)
950    }
951    /// Shorthand for a write-only binding.
952    pub fn write(binding: u32, buffer_id: BufferId) -> Self {
953        Self::new(binding, buffer_id, BufferUsage::WriteOnly)
954    }
955    /// Shorthand for a read-write binding.
956    pub fn read_write(binding: u32, buffer_id: BufferId) -> Self {
957        Self::new(binding, buffer_id, BufferUsage::ReadWrite)
958    }
959    /// Shorthand for a uniform binding.
960    pub fn uniform(binding: u32, buffer_id: BufferId) -> Self {
961        Self::new(binding, buffer_id, BufferUsage::Uniform)
962    }
963}
964/// Newtype handle for GPU buffers in the dispatcher model.
965#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
966pub struct BufferId(pub u32);