Skip to main content

oxigdal_gpu/backends/
metal.rs

1//! Metal-specific optimizations for Apple platforms.
2//!
3//! This module provides Metal Performance Shaders integration, unified memory
4//! optimization for Apple Silicon, and threadgroup memory optimization.
5
6use crate::context::GpuContext;
7use crate::error::{GpuError, GpuResult};
8use std::collections::HashMap;
9use tracing::{debug, info};
10
11/// Metal optimization configuration.
12#[derive(Debug, Clone)]
13pub struct MetalOptimizationConfig {
14    /// Enable Metal Performance Shaders (MPS).
15    pub enable_mps: bool,
16    /// Enable unified memory optimization.
17    pub enable_unified_memory: bool,
18    /// Enable threadgroup memory optimization.
19    pub enable_threadgroup_memory: bool,
20    /// Enable argument buffers.
21    pub enable_argument_buffers: bool,
22    /// Preferred threadgroup size.
23    pub threadgroup_size: (u32, u32, u32),
24}
25
26impl Default for MetalOptimizationConfig {
27    fn default() -> Self {
28        Self {
29            enable_mps: true,
30            enable_unified_memory: true,
31            enable_threadgroup_memory: true,
32            enable_argument_buffers: true,
33            threadgroup_size: (256, 1, 1),
34        }
35    }
36}
37
38/// Metal feature set detector.
39pub struct MetalFeatureDetector {
40    features: MetalFeatures,
41}
42
43#[derive(Debug, Clone)]
44pub struct MetalFeatures {
45    /// Device family (Apple GPU generation).
46    pub family: MetalFamily,
47    /// Supports Apple Silicon unified memory.
48    pub unified_memory: bool,
49    /// Supports MPS.
50    pub mps_support: bool,
51    /// Maximum threadgroup memory.
52    pub max_threadgroup_memory: u64,
53    /// SIMD group size (similar to warp/subgroup).
54    pub simd_width: u32,
55    /// Supports argument buffers.
56    pub argument_buffers: bool,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum MetalFamily {
61    /// Apple A-series (iOS/iPadOS).
62    Apple7,
63    Apple8,
64    Apple9,
65    /// Apple M-series (macOS).
66    Mac2,
67    Mac3,
68    Mac4,
69    /// Unknown or fallback.
70    Unknown,
71}
72
73impl Default for MetalFeatures {
74    fn default() -> Self {
75        Self {
76            family: MetalFamily::Mac2,
77            unified_memory: true,
78            mps_support: true,
79            max_threadgroup_memory: 32 * 1024,
80            simd_width: 32,
81            argument_buffers: true,
82        }
83    }
84}
85
86impl MetalFeatureDetector {
87    /// Create a new feature detector.
88    pub fn new(context: &GpuContext) -> Self {
89        let features = Self::detect_features(context);
90        info!(
91            "Metal features: family={:?}, unified_memory={}, mps={}, simd_width={}",
92            features.family, features.unified_memory, features.mps_support, features.simd_width
93        );
94
95        Self { features }
96    }
97
98    /// Get detected features.
99    pub fn features(&self) -> &MetalFeatures {
100        &self.features
101    }
102
103    fn detect_features(context: &GpuContext) -> MetalFeatures {
104        let adapter_info = context.adapter_info();
105        let name = adapter_info.name.to_lowercase();
106
107        let family = if name.contains("m1")
108            || name.contains("m2")
109            || name.contains("m3")
110            || name.contains("m4")
111        {
112            if name.contains("m4") {
113                MetalFamily::Mac4
114            } else if name.contains("m3") {
115                MetalFamily::Mac3
116            } else {
117                MetalFamily::Mac2
118            }
119        } else if name.contains("apple") {
120            MetalFamily::Apple7
121        } else {
122            MetalFamily::Unknown
123        };
124
125        let unified_memory = matches!(
126            family,
127            MetalFamily::Mac2 | MetalFamily::Mac3 | MetalFamily::Mac4
128        );
129
130        MetalFeatures {
131            family,
132            unified_memory,
133            mps_support: true,
134            max_threadgroup_memory: 32 * 1024,
135            simd_width: 32,
136            argument_buffers: true,
137        }
138    }
139}
140
141/// Metal shader optimizer.
142pub struct MetalShaderOptimizer {
143    features: MetalFeatures,
144    config: MetalOptimizationConfig,
145}
146
147impl MetalShaderOptimizer {
148    /// Create a new shader optimizer.
149    pub fn new(features: MetalFeatures, config: MetalOptimizationConfig) -> Self {
150        Self { features, config }
151    }
152
153    /// Optimize shader code for Metal.
154    pub fn optimize_shader(&self, shader_code: &str) -> String {
155        let mut optimized = shader_code.to_string();
156
157        // Add SIMD group size
158        if !optimized.contains("SIMD_WIDTH") {
159            let simd_decl = format!("\nconst SIMD_WIDTH: u32 = {}u;\n", self.features.simd_width);
160            optimized.insert_str(0, &simd_decl);
161        }
162
163        // Add threadgroup memory helpers
164        if self.config.enable_threadgroup_memory {
165            optimized.push_str(Self::threadgroup_memory_helpers());
166        }
167
168        // Add unified memory hints
169        if self.config.enable_unified_memory && self.features.unified_memory {
170            optimized.insert_str(0, "\n// Unified Memory Optimized for Apple Silicon\n");
171        }
172
173        optimized
174    }
175
176    /// Calculate optimal threadgroup size.
177    pub fn calculate_threadgroup_size(&self, num_elements: u64) -> (u32, u32, u32) {
178        let max_threads = 1024; // Metal limit
179
180        if num_elements <= max_threads as u64 {
181            return (num_elements as u32, 1, 1);
182        }
183
184        // Use configured threadgroup size
185        self.config.threadgroup_size
186    }
187
188    fn threadgroup_memory_helpers() -> &'static str {
189        r#"
190// Metal threadgroup memory helpers
191// In WGSL, this uses workgroup memory
192
193// Threadgroup barrier
194fn threadgroup_barrier() {
195    workgroupBarrier();
196}
197
198// Threadgroup memory fence
199fn threadgroup_memory_fence() {
200    storageBarrier();
201}
202"#
203    }
204}
205
206/// Metal Performance Shaders (MPS) integration.
207pub struct MetalPerformanceShaders {
208    available_kernels: HashMap<String, MPSKernel>,
209}
210
211#[derive(Debug, Clone)]
212struct MPSKernel {
213    name: String,
214    kernel_type: MPSKernelType,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum MPSKernelType {
219    /// Matrix multiplication.
220    MatrixMultiplication,
221    /// Convolution.
222    Convolution,
223    /// Image filtering.
224    ImageFilter,
225    /// Reduction operations.
226    Reduction,
227    /// Neural network operations.
228    NeuralNetwork,
229}
230
231impl MetalPerformanceShaders {
232    /// Create a new MPS integration.
233    pub fn new() -> Self {
234        let mut available_kernels = HashMap::new();
235
236        // Register available MPS kernels
237        available_kernels.insert(
238            "matmul".to_string(),
239            MPSKernel {
240                name: "matmul".to_string(),
241                kernel_type: MPSKernelType::MatrixMultiplication,
242            },
243        );
244
245        available_kernels.insert(
246            "conv2d".to_string(),
247            MPSKernel {
248                name: "conv2d".to_string(),
249                kernel_type: MPSKernelType::Convolution,
250            },
251        );
252
253        available_kernels.insert(
254            "reduce_sum".to_string(),
255            MPSKernel {
256                name: "reduce_sum".to_string(),
257                kernel_type: MPSKernelType::Reduction,
258            },
259        );
260
261        Self { available_kernels }
262    }
263
264    /// Check if a kernel is available.
265    pub fn is_available(&self, name: &str) -> bool {
266        self.available_kernels.contains_key(name)
267    }
268
269    /// Get kernel information.
270    pub fn get_kernel(&self, name: &str) -> Option<&MPSKernel> {
271        self.available_kernels.get(name)
272    }
273
274    /// List all available kernels.
275    pub fn list_kernels(&self) -> Vec<String> {
276        self.available_kernels.keys().cloned().collect()
277    }
278
279    /// Generate shader code that uses MPS-optimized operations.
280    pub fn generate_mps_shader(&self, kernel_type: MPSKernelType) -> String {
281        match kernel_type {
282            MPSKernelType::MatrixMultiplication => self.generate_matmul_shader(),
283            MPSKernelType::Convolution => self.generate_conv_shader(),
284            MPSKernelType::ImageFilter => self.generate_filter_shader(),
285            MPSKernelType::Reduction => self.generate_reduction_shader(),
286            MPSKernelType::NeuralNetwork => self.generate_nn_shader(),
287        }
288    }
289
290    fn generate_matmul_shader(&self) -> String {
291        r#"
292// Metal-optimized matrix multiplication
293@group(0) @binding(0) var<storage, read> matrix_a: array<f32>;
294@group(0) @binding(1) var<storage, read> matrix_b: array<f32>;
295@group(0) @binding(2) var<storage, read_write> matrix_c: array<f32>;
296
297struct MatmulParams {
298    m: u32,
299    n: u32,
300    k: u32,
301}
302
303@group(1) @binding(0) var<uniform> params: MatmulParams;
304
305@compute @workgroup_size(16, 16)
306fn matmul(@builtin(global_invocation_id) global_id: vec3<u32>) {
307    let row = global_id.x;
308    let col = global_id.y;
309
310    if (row >= params.m || col >= params.n) {
311        return;
312    }
313
314    var sum = 0.0;
315    for (var i = 0u; i < params.k; i++) {
316        let a_val = matrix_a[row * params.k + i];
317        let b_val = matrix_b[i * params.n + col];
318        sum += a_val * b_val;
319    }
320
321    matrix_c[row * params.n + col] = sum;
322}
323"#
324        .to_string()
325    }
326
327    fn generate_conv_shader(&self) -> String {
328        r#"
329// Metal-optimized 2D convolution
330@group(0) @binding(0) var<storage, read> input: array<f32>;
331@group(0) @binding(1) var<storage, read> kernel: array<f32>;
332@group(0) @binding(2) var<storage, read_write> output: array<f32>;
333
334struct ConvParams {
335    width: u32,
336    height: u32,
337    kernel_size: u32,
338}
339
340@group(1) @binding(0) var<uniform> params: ConvParams;
341
342@compute @workgroup_size(16, 16)
343fn conv2d(@builtin(global_invocation_id) global_id: vec3<u32>) {
344    let x = global_id.x;
345    let y = global_id.y;
346
347    if (x >= params.width || y >= params.height) {
348        return;
349    }
350
351    var sum = 0.0;
352    let half_k = params.kernel_size / 2u;
353
354    for (var ky = 0u; ky < params.kernel_size; ky++) {
355        for (var kx = 0u; kx < params.kernel_size; kx++) {
356            let ix = i32(x) + i32(kx) - i32(half_k);
357            let iy = i32(y) + i32(ky) - i32(half_k);
358
359            if (ix >= 0 && ix < i32(params.width) && iy >= 0 && iy < i32(params.height)) {
360                let input_idx = u32(iy) * params.width + u32(ix);
361                let kernel_idx = ky * params.kernel_size + kx;
362                sum += input[input_idx] * kernel[kernel_idx];
363            }
364        }
365    }
366
367    output[y * params.width + x] = sum;
368}
369"#
370        .to_string()
371    }
372
373    /// Generate a **separable Gaussian filter** as two WGSL compute passes.
374    ///
375    /// The MPS `MPSImageGaussianBlur` primitive is implemented on-GPU as a
376    /// separable convolution: a 1-D Gaussian kernel applied horizontally, then
377    /// the same kernel applied vertically.  This shader exposes two entry
378    /// points — `gaussian_horizontal` and `gaussian_vertical` — that share the
379    /// same bind-group layout so the caller can ping-pong `src`→`dst` between
380    /// them with an intermediate buffer.
381    ///
382    /// Bindings:
383    /// * `@group(0) @binding(0)` — `src` input samples (row-major `f32`).
384    /// * `@group(0) @binding(1)` — `weights`, the 1-D kernel of length
385    ///   `2 * radius + 1` (should sum to 1 for an energy-preserving blur).
386    /// * `@group(0) @binding(2)` — `dst` output samples.
387    /// * `@group(1) @binding(0)` — `FilterParams { width, height, radius }`.
388    ///
389    /// Out-of-bounds taps clamp to the edge (`GL_CLAMP_TO_EDGE` semantics).
390    fn generate_filter_shader(&self) -> String {
391        r#"
392// Metal-style separable Gaussian filter (two passes share this layout).
393@group(0) @binding(0) var<storage, read>       src: array<f32>;
394@group(0) @binding(1) var<storage, read>       weights: array<f32>;
395@group(0) @binding(2) var<storage, read_write> dst: array<f32>;
396
397struct FilterParams {
398    width: u32,
399    height: u32,
400    radius: u32,
401    _pad: u32,
402}
403
404@group(1) @binding(0) var<uniform> params: FilterParams;
405
406@compute @workgroup_size(16, 16)
407fn gaussian_horizontal(@builtin(global_invocation_id) global_id: vec3<u32>) {
408    let x = global_id.x;
409    let y = global_id.y;
410    if (x >= params.width || y >= params.height) {
411        return;
412    }
413    let r = i32(params.radius);
414    var acc = 0.0;
415    for (var k = -r; k <= r; k = k + 1) {
416        var sx = i32(x) + k;
417        if (sx < 0) { sx = 0; }
418        if (sx > i32(params.width) - 1) { sx = i32(params.width) - 1; }
419        let w = weights[u32(k + r)];
420        acc = acc + w * src[y * params.width + u32(sx)];
421    }
422    dst[y * params.width + x] = acc;
423}
424
425@compute @workgroup_size(16, 16)
426fn gaussian_vertical(@builtin(global_invocation_id) global_id: vec3<u32>) {
427    let x = global_id.x;
428    let y = global_id.y;
429    if (x >= params.width || y >= params.height) {
430        return;
431    }
432    let r = i32(params.radius);
433    var acc = 0.0;
434    for (var k = -r; k <= r; k = k + 1) {
435        var sy = i32(y) + k;
436        if (sy < 0) { sy = 0; }
437        if (sy > i32(params.height) - 1) { sy = i32(params.height) - 1; }
438        let w = weights[u32(k + r)];
439        acc = acc + w * src[u32(sy) * params.width + x];
440    }
441    dst[y * params.width + x] = acc;
442}
443"#
444        .to_string()
445    }
446
447    /// Generate a **two-pass workgroup reduction** (sum / min / max).
448    ///
449    /// Each workgroup cooperatively reduces a 256-element chunk into one
450    /// partial via shared memory and a `workgroupBarrier()` tree reduction,
451    /// writing `partials[workgroup_id.x]`.  Reducing a buffer of `N` elements
452    /// to a single scalar is a two-pass operation: dispatch over the `N`
453    /// inputs (producing `ceil(N / 256)` partials), then dispatch the same
454    /// kernel over those partials.
455    ///
456    /// Three entry points are provided — `reduce_sum`, `reduce_min`,
457    /// `reduce_max` — using the identity element (`0`, `+3.4e38`, `-3.4e38`)
458    /// for out-of-range invocations.
459    ///
460    /// Bindings:
461    /// * `@group(0) @binding(0)` — `input` (row-major `f32`).
462    /// * `@group(0) @binding(1)` — `partials` output (`read_write`).
463    /// * `@group(1) @binding(0)` — `ReduceParams { n }` element count.
464    fn generate_reduction_shader(&self) -> String {
465        r#"
466// Metal-style two-pass workgroup reduction.
467@group(0) @binding(0) var<storage, read>       input: array<f32>;
468@group(0) @binding(1) var<storage, read_write> partials: array<f32>;
469
470struct ReduceParams {
471    n: u32,
472    _pad0: u32,
473    _pad1: u32,
474    _pad2: u32,
475}
476
477@group(1) @binding(0) var<uniform> params: ReduceParams;
478
479const REDUCE_WG: u32 = 256u;
480var<workgroup> red_scratch: array<f32, 256>;
481
482@compute @workgroup_size(256)
483fn reduce_sum(@builtin(global_invocation_id) gid: vec3<u32>,
484              @builtin(local_invocation_index) lid: u32,
485              @builtin(workgroup_id) wid: vec3<u32>) {
486    var v = 0.0;
487    if (gid.x < params.n) { v = input[gid.x]; }
488    red_scratch[lid] = v;
489    workgroupBarrier();
490    var stride = REDUCE_WG / 2u;
491    loop {
492        if (stride == 0u) { break; }
493        if (lid < stride) {
494            red_scratch[lid] = red_scratch[lid] + red_scratch[lid + stride];
495        }
496        workgroupBarrier();
497        stride = stride / 2u;
498    }
499    if (lid == 0u) { partials[wid.x] = red_scratch[0]; }
500}
501
502@compute @workgroup_size(256)
503fn reduce_min(@builtin(global_invocation_id) gid: vec3<u32>,
504              @builtin(local_invocation_index) lid: u32,
505              @builtin(workgroup_id) wid: vec3<u32>) {
506    var v = 3.4e38;
507    if (gid.x < params.n) { v = input[gid.x]; }
508    red_scratch[lid] = v;
509    workgroupBarrier();
510    var stride = REDUCE_WG / 2u;
511    loop {
512        if (stride == 0u) { break; }
513        if (lid < stride) {
514            red_scratch[lid] = min(red_scratch[lid], red_scratch[lid + stride]);
515        }
516        workgroupBarrier();
517        stride = stride / 2u;
518    }
519    if (lid == 0u) { partials[wid.x] = red_scratch[0]; }
520}
521
522@compute @workgroup_size(256)
523fn reduce_max(@builtin(global_invocation_id) gid: vec3<u32>,
524              @builtin(local_invocation_index) lid: u32,
525              @builtin(workgroup_id) wid: vec3<u32>) {
526    var v = -3.4e38;
527    if (gid.x < params.n) { v = input[gid.x]; }
528    red_scratch[lid] = v;
529    workgroupBarrier();
530    var stride = REDUCE_WG / 2u;
531    loop {
532        if (stride == 0u) { break; }
533        if (lid < stride) {
534            red_scratch[lid] = max(red_scratch[lid], red_scratch[lid + stride]);
535        }
536        workgroupBarrier();
537        stride = stride / 2u;
538    }
539    if (lid == 0u) { partials[wid.x] = red_scratch[0]; }
540}
541"#
542        .to_string()
543    }
544
545    /// Generate a **ReLU + tiled matrix multiply** compute shader.
546    ///
547    /// Models the fused GEMM+activation building block used by MPS neural
548    /// network graphs: `C = relu(A × B)` where `A` is `M × K`, `B` is `K × N`,
549    /// both row-major.  The kernel loads 16 × 16 tiles of `A` and `B` into
550    /// workgroup-shared memory, accumulates the tile products with a
551    /// `workgroupBarrier()` between tiles, then applies `max(sum, 0)`.
552    ///
553    /// Bindings:
554    /// * `@group(0) @binding(0)` — `mat_a` (`M × K`, `f32`).
555    /// * `@group(0) @binding(1)` — `mat_b` (`K × N`, `f32`).
556    /// * `@group(0) @binding(2)` — `mat_c` output (`M × N`, `read_write`).
557    /// * `@group(1) @binding(0)` — `NnDims { M, N, K }`.
558    fn generate_nn_shader(&self) -> String {
559        r#"
560// Metal-style fused ReLU + tiled GEMM: C = relu(A * B).
561@group(0) @binding(0) var<storage, read>       mat_a: array<f32>;
562@group(0) @binding(1) var<storage, read>       mat_b: array<f32>;
563@group(0) @binding(2) var<storage, read_write> mat_c: array<f32>;
564
565struct NnDims {
566    M: u32,
567    N: u32,
568    K: u32,
569    _pad: u32,
570}
571
572@group(1) @binding(0) var<uniform> dims: NnDims;
573
574const TILE: u32 = 16u;
575var<workgroup> tile_a: array<f32, 256>;
576var<workgroup> tile_b: array<f32, 256>;
577
578@compute @workgroup_size(16, 16)
579fn matmul_relu(@builtin(global_invocation_id) gid: vec3<u32>,
580               @builtin(local_invocation_id) lid: vec3<u32>) {
581    let row = gid.y;
582    let col = gid.x;
583    let num_tiles = (dims.K + TILE - 1u) / TILE;
584
585    var sum = 0.0;
586    for (var t = 0u; t < num_tiles; t = t + 1u) {
587        let a_col = t * TILE + lid.x;
588        if (row < dims.M && a_col < dims.K) {
589            tile_a[lid.y * TILE + lid.x] = mat_a[row * dims.K + a_col];
590        } else {
591            tile_a[lid.y * TILE + lid.x] = 0.0;
592        }
593        let b_row = t * TILE + lid.y;
594        if (b_row < dims.K && col < dims.N) {
595            tile_b[lid.y * TILE + lid.x] = mat_b[b_row * dims.N + col];
596        } else {
597            tile_b[lid.y * TILE + lid.x] = 0.0;
598        }
599        workgroupBarrier();
600
601        for (var k = 0u; k < TILE; k = k + 1u) {
602            sum = sum + tile_a[lid.y * TILE + k] * tile_b[k * TILE + lid.x];
603        }
604        workgroupBarrier();
605    }
606
607    if (row < dims.M && col < dims.N) {
608        mat_c[row * dims.N + col] = max(sum, 0.0);
609    }
610}
611"#
612        .to_string()
613    }
614}
615
616impl Default for MetalPerformanceShaders {
617    fn default() -> Self {
618        Self::new()
619    }
620}
621
622/// Unified memory optimizer for Apple Silicon.
623pub struct UnifiedMemoryOptimizer {
624    enabled: bool,
625}
626
627impl UnifiedMemoryOptimizer {
628    /// Create a new unified memory optimizer.
629    pub fn new(enabled: bool) -> Self {
630        Self { enabled }
631    }
632
633    /// Check if unified memory is enabled.
634    pub fn is_enabled(&self) -> bool {
635        self.enabled
636    }
637
638    /// Optimize buffer allocation for unified memory.
639    pub fn optimize_allocation(&self, size: u64) -> AllocationHint {
640        if !self.enabled {
641            return AllocationHint::Standard;
642        }
643
644        // For unified memory, prefer shared buffers
645        if size < 1024 * 1024 {
646            // Small allocations (<1MB) - use shared memory
647            AllocationHint::Shared
648        } else {
649            // Large allocations - use private memory with streaming
650            AllocationHint::Private
651        }
652    }
653
654    /// Calculate optimal memory access pattern for unified memory.
655    pub fn optimize_access_pattern(&self, width: u32, height: u32) -> AccessPattern {
656        AccessPattern {
657            width,
658            height,
659            prefer_linear: true,
660            prefer_tiled: false,
661        }
662    }
663}
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
666pub enum AllocationHint {
667    /// Standard GPU allocation.
668    Standard,
669    /// Shared CPU/GPU memory.
670    Shared,
671    /// Private GPU memory.
672    Private,
673    /// Managed memory with explicit synchronization.
674    Managed,
675}
676
677#[derive(Debug, Clone)]
678pub struct AccessPattern {
679    /// Width of the data.
680    pub width: u32,
681    /// Height of the data.
682    pub height: u32,
683    /// Prefer linear memory layout.
684    pub prefer_linear: bool,
685    /// Prefer tiled memory layout.
686    pub prefer_tiled: bool,
687}
688
689/// Metal argument buffer manager.
690pub struct ArgumentBufferManager {
691    buffers: HashMap<u32, ArgumentBuffer>,
692    next_id: u32,
693}
694
695#[derive(Debug, Clone)]
696struct ArgumentBuffer {
697    id: u32,
698    name: String,
699    arguments: Vec<ArgumentDescriptor>,
700}
701
702#[derive(Debug, Clone)]
703struct ArgumentDescriptor {
704    name: String,
705    binding: u32,
706    arg_type: ArgumentType,
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Eq)]
710pub enum ArgumentType {
711    /// Storage buffer.
712    Buffer,
713    /// Texture.
714    Texture,
715    /// Sampler.
716    Sampler,
717}
718
719impl ArgumentBufferManager {
720    /// Create a new argument buffer manager.
721    pub fn new() -> Self {
722        Self {
723            buffers: HashMap::new(),
724            next_id: 0,
725        }
726    }
727
728    /// Create an argument buffer.
729    pub fn create(&mut self, name: String) -> u32 {
730        let id = self.next_id;
731        self.next_id += 1;
732
733        self.buffers.insert(
734            id,
735            ArgumentBuffer {
736                id,
737                name: name.clone(),
738                arguments: Vec::new(),
739            },
740        );
741
742        debug!("Created argument buffer '{}' (ID: {})", name, id);
743
744        id
745    }
746
747    /// Add an argument to a buffer.
748    ///
749    /// # Errors
750    ///
751    /// Returns an error if buffer not found.
752    pub fn add_argument(
753        &mut self,
754        buffer_id: u32,
755        name: String,
756        binding: u32,
757        arg_type: ArgumentType,
758    ) -> GpuResult<()> {
759        let buffer = self
760            .buffers
761            .get_mut(&buffer_id)
762            .ok_or_else(|| GpuError::invalid_buffer("Argument buffer not found"))?;
763
764        buffer.arguments.push(ArgumentDescriptor {
765            name: name.clone(),
766            binding,
767            arg_type,
768        });
769
770        debug!(
771            "Added argument '{}' to buffer {} at binding {}",
772            name, buffer_id, binding
773        );
774
775        Ok(())
776    }
777
778    /// Get argument buffer.
779    pub fn get(&self, buffer_id: u32) -> Option<&ArgumentBuffer> {
780        self.buffers.get(&buffer_id)
781    }
782
783    /// Destroy an argument buffer.
784    pub fn destroy(&mut self, buffer_id: u32) {
785        if let Some(buffer) = self.buffers.remove(&buffer_id) {
786            debug!("Destroyed argument buffer '{}'", buffer.name);
787        }
788    }
789}
790
791impl Default for ArgumentBufferManager {
792    fn default() -> Self {
793        Self::new()
794    }
795}
796
797/// Metal threadgroup memory allocator.
798pub struct ThreadgroupMemoryAllocator {
799    max_memory: u64,
800    allocated: u64,
801    allocations: HashMap<u32, ThreadgroupAllocation>,
802    next_id: u32,
803}
804
805#[derive(Debug, Clone)]
806struct ThreadgroupAllocation {
807    id: u32,
808    size: u64,
809    offset: u64,
810}
811
812impl ThreadgroupMemoryAllocator {
813    /// Create a new threadgroup memory allocator.
814    pub fn new(max_memory: u64) -> Self {
815        Self {
816            max_memory,
817            allocated: 0,
818            allocations: HashMap::new(),
819            next_id: 0,
820        }
821    }
822
823    /// Allocate threadgroup memory.
824    ///
825    /// # Errors
826    ///
827    /// Returns an error if allocation exceeds limit.
828    pub fn allocate(&mut self, size: u64) -> GpuResult<u32> {
829        if self.allocated + size > self.max_memory {
830            return Err(GpuError::out_of_memory(
831                size,
832                self.max_memory - self.allocated,
833            ));
834        }
835
836        let id = self.next_id;
837        self.next_id += 1;
838
839        let offset = self.allocated;
840        self.allocated += size;
841
842        self.allocations
843            .insert(id, ThreadgroupAllocation { id, size, offset });
844
845        debug!(
846            "Allocated {} bytes of threadgroup memory at offset {}",
847            size, offset
848        );
849
850        Ok(id)
851    }
852
853    /// Free threadgroup memory.
854    ///
855    /// # Errors
856    ///
857    /// Returns an error if allocation not found.
858    pub fn free(&mut self, id: u32) -> GpuResult<()> {
859        let alloc = self
860            .allocations
861            .remove(&id)
862            .ok_or_else(|| GpuError::invalid_buffer("Allocation not found"))?;
863
864        self.allocated = self.allocated.saturating_sub(alloc.size);
865
866        debug!("Freed {} bytes of threadgroup memory", alloc.size);
867
868        Ok(())
869    }
870
871    /// Get current usage.
872    pub fn usage(&self) -> (u64, u64) {
873        (self.allocated, self.max_memory)
874    }
875
876    /// Reset allocator.
877    pub fn reset(&mut self) {
878        self.allocations.clear();
879        self.allocated = 0;
880    }
881}
882
883/// SIMD group operations for Metal.
884///
885/// Metal `simd_shuffle*` / `simd_sum` intrinsics compile to MSL and are gated
886/// on real Metal hardware.  The WGSL emitted here maps each onto the portable
887/// WGSL subgroup built-ins when the device exposes
888/// `wgpu::Features::SUBGROUP` (`native == true`); otherwise it emits a
889/// `workgroupBarrier()`-synchronised workgroup-shared-memory emulation with
890/// matching semantics *within a workgroup*.  Every helper takes trailing
891/// `(lid: u32, n: u32)` — the invocation's `local_invocation_index` and the
892/// active-invocation count — which the native path ignores.
893pub struct SimdGroupOperations;
894
895impl SimdGroupOperations {
896    /// Generate shader code for SIMD group shuffle operations.
897    pub fn simd_shuffle_shader(native: bool) -> String {
898        if native {
899            r#"
900// Native SIMD-group shuffle == WGSL subgroup shuffle (Features::SUBGROUP).
901fn simd_shuffle(value: f32, lane: u32, lid: u32, n: u32) -> f32 { return subgroupShuffle(value, lane); }
902fn simd_shuffle_down(value: f32, delta: u32, lid: u32, n: u32) -> f32 { return subgroupShuffleDown(value, delta); }
903fn simd_shuffle_up(value: f32, delta: u32, lid: u32, n: u32) -> f32 { return subgroupShuffleUp(value, delta); }
904fn simd_shuffle_xor(value: f32, mask: u32, lid: u32, n: u32) -> f32 { return subgroupShuffleXor(value, mask); }
905"#
906            .to_string()
907        } else {
908            r#"
909// Emulated SIMD-group shuffle — workgroup-shared memory + workgroupBarrier().
910// Out-of-range source lanes return the caller's own value.
911var<workgroup> simd_shfl_scratch: array<f32, 256>;
912fn simd_shuffle(value: f32, lane: u32, lid: u32, n: u32) -> f32 {
913    simd_shfl_scratch[lid] = value;
914    workgroupBarrier();
915    var idx = lane;
916    if (idx >= n) { idx = lid; }
917    let result = simd_shfl_scratch[idx];
918    workgroupBarrier();
919    return result;
920}
921fn simd_shuffle_down(value: f32, delta: u32, lid: u32, n: u32) -> f32 {
922    simd_shfl_scratch[lid] = value;
923    workgroupBarrier();
924    var idx = lid + delta;
925    if (idx >= n) { idx = lid; }
926    let result = simd_shfl_scratch[idx];
927    workgroupBarrier();
928    return result;
929}
930fn simd_shuffle_up(value: f32, delta: u32, lid: u32, n: u32) -> f32 {
931    simd_shfl_scratch[lid] = value;
932    workgroupBarrier();
933    var idx = lid;
934    if (lid >= delta) { idx = lid - delta; }
935    let result = simd_shfl_scratch[idx];
936    workgroupBarrier();
937    return result;
938}
939fn simd_shuffle_xor(value: f32, mask: u32, lid: u32, n: u32) -> f32 {
940    simd_shfl_scratch[lid] = value;
941    workgroupBarrier();
942    var idx = lid ^ mask;
943    if (idx >= n) { idx = lid; }
944    let result = simd_shfl_scratch[idx];
945    workgroupBarrier();
946    return result;
947}
948"#
949            .to_string()
950        }
951    }
952
953    /// Generate shader code for SIMD group reductions.
954    ///
955    /// Native reductions broadcast the reduced value to every lane of the
956    /// subgroup (matching Metal's `simd_sum` family).  The emulation reduces
957    /// across the whole workgroup (`n` active invocations, `n <= 256`) and
958    /// likewise returns the result on every invocation.
959    pub fn simd_reduce_shader(native: bool) -> String {
960        if native {
961            r#"
962// Native SIMD-group reduction == WGSL subgroup reduction (Features::SUBGROUP).
963fn simd_sum(value: f32, lid: u32, n: u32) -> f32 { return subgroupAdd(value); }
964fn simd_max(value: f32, lid: u32, n: u32) -> f32 { return subgroupMax(value); }
965fn simd_min(value: f32, lid: u32, n: u32) -> f32 { return subgroupMin(value); }
966fn simd_product(value: f32, lid: u32, n: u32) -> f32 { return subgroupMul(value); }
967"#
968            .to_string()
969        } else {
970            r#"
971// Emulated SIMD-group reduction — workgroup-wide tree reduction via shared
972// memory + workgroupBarrier(); result broadcast to every invocation.
973var<workgroup> simd_red_scratch: array<f32, 256>;
974fn simd_sum(value: f32, lid: u32, n: u32) -> f32 {
975    simd_red_scratch[lid] = value;
976    workgroupBarrier();
977    var stride = 1u;
978    loop {
979        if (stride >= n) { break; }
980        let idx = lid * stride * 2u;
981        if (idx + stride < n) {
982            simd_red_scratch[idx] = simd_red_scratch[idx] + simd_red_scratch[idx + stride];
983        }
984        stride = stride * 2u;
985        workgroupBarrier();
986    }
987    let result = simd_red_scratch[0];
988    workgroupBarrier();
989    return result;
990}
991fn simd_max(value: f32, lid: u32, n: u32) -> f32 {
992    simd_red_scratch[lid] = value;
993    workgroupBarrier();
994    var stride = 1u;
995    loop {
996        if (stride >= n) { break; }
997        let idx = lid * stride * 2u;
998        if (idx + stride < n) {
999            simd_red_scratch[idx] = max(simd_red_scratch[idx], simd_red_scratch[idx + stride]);
1000        }
1001        stride = stride * 2u;
1002        workgroupBarrier();
1003    }
1004    let result = simd_red_scratch[0];
1005    workgroupBarrier();
1006    return result;
1007}
1008fn simd_min(value: f32, lid: u32, n: u32) -> f32 {
1009    simd_red_scratch[lid] = value;
1010    workgroupBarrier();
1011    var stride = 1u;
1012    loop {
1013        if (stride >= n) { break; }
1014        let idx = lid * stride * 2u;
1015        if (idx + stride < n) {
1016            simd_red_scratch[idx] = min(simd_red_scratch[idx], simd_red_scratch[idx + stride]);
1017        }
1018        stride = stride * 2u;
1019        workgroupBarrier();
1020    }
1021    let result = simd_red_scratch[0];
1022    workgroupBarrier();
1023    return result;
1024}
1025fn simd_product(value: f32, lid: u32, n: u32) -> f32 {
1026    simd_red_scratch[lid] = value;
1027    workgroupBarrier();
1028    var stride = 1u;
1029    loop {
1030        if (stride >= n) { break; }
1031        let idx = lid * stride * 2u;
1032        if (idx + stride < n) {
1033            simd_red_scratch[idx] = simd_red_scratch[idx] * simd_red_scratch[idx + stride];
1034        }
1035        stride = stride * 2u;
1036        workgroupBarrier();
1037    }
1038    let result = simd_red_scratch[0];
1039    workgroupBarrier();
1040    return result;
1041}
1042"#
1043            .to_string()
1044        }
1045    }
1046
1047    /// Return `true` when the device exposes native subgroup intrinsics.
1048    ///
1049    /// Convenience wrapper to pick the `native` argument for
1050    /// [`Self::simd_shuffle_shader`] / [`Self::simd_reduce_shader`] from a live
1051    /// [`GpuContext`].
1052    pub fn native_subgroups(context: &GpuContext) -> bool {
1053        context
1054            .device()
1055            .features()
1056            .contains(wgpu::Features::SUBGROUP)
1057    }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062    use super::*;
1063
1064    #[test]
1065    fn test_metal_features() {
1066        let features = MetalFeatures::default();
1067        assert_eq!(features.simd_width, 32);
1068        assert!(features.unified_memory);
1069        assert!(features.mps_support);
1070    }
1071
1072    #[test]
1073    fn test_metal_performance_shaders() {
1074        let mps = MetalPerformanceShaders::new();
1075        assert!(mps.is_available("matmul"));
1076        assert!(mps.is_available("conv2d"));
1077
1078        let kernels = mps.list_kernels();
1079        assert!(!kernels.is_empty());
1080    }
1081
1082    #[test]
1083    fn test_unified_memory_optimizer() {
1084        let optimizer = UnifiedMemoryOptimizer::new(true);
1085        assert!(optimizer.is_enabled());
1086
1087        let hint = optimizer.optimize_allocation(512 * 1024);
1088        assert_eq!(hint, AllocationHint::Shared);
1089
1090        let hint = optimizer.optimize_allocation(2 * 1024 * 1024);
1091        assert_eq!(hint, AllocationHint::Private);
1092    }
1093
1094    #[test]
1095    fn test_argument_buffer_manager() {
1096        let mut manager = ArgumentBufferManager::new();
1097
1098        let buffer_id = manager.create("test_args".to_string());
1099        manager
1100            .add_argument(buffer_id, "input".to_string(), 0, ArgumentType::Buffer)
1101            .expect("Failed to add argument");
1102
1103        let buffer = manager.get(buffer_id).expect("Buffer not found");
1104        assert_eq!(buffer.arguments.len(), 1);
1105    }
1106
1107    #[test]
1108    fn test_threadgroup_memory_allocator() {
1109        let mut allocator = ThreadgroupMemoryAllocator::new(32 * 1024);
1110
1111        let id1 = allocator.allocate(1024).expect("Failed to allocate");
1112        let _id2 = allocator.allocate(2048).expect("Failed to allocate");
1113
1114        let (used, total) = allocator.usage();
1115        assert_eq!(used, 3072);
1116        assert_eq!(total, 32 * 1024);
1117
1118        allocator.free(id1).expect("Failed to free");
1119        let (used, _) = allocator.usage();
1120        assert_eq!(used, 2048);
1121    }
1122}