Skip to main content

optirs_gpu/memory/vendors/
metal_backend.rs

1// Metal backend for GPU memory management
2//
3// This module provides Apple Metal-specific memory management functionality,
4// including device memory allocation, Metal command buffers, and performance
5// optimization features specific to Apple Silicon GPUs.
6//
7// # This is a host-memory simulation, not real Metal
8//
9// Unlike `crate::optimizers`/`crate::shaders`, which drive a *real* Metal
10// device through `scirs2_core::gpu::GpuContext` (compiled `.metal` sources,
11// real `MTLBuffer`s, real dispatch), this standalone module does not: it has
12// no `GpuContext` of its own, so "private", "shared", "managed" and
13// "memoryless" storage are all the *same* system-heap allocation (see
14// `sim_alloc`/`sim_dealloc` below), and `MetalDeviceProperties`/`MetalStats`
15// are example numbers, not a query of the real GPU. This module models the
16// `MTLBuffer` memory-management *API shape* for testing that shape in
17// isolation; treat every allocation as host memory and every device number
18// as illustrative. For real Metal compute, use [`crate::optimizers`].
19//
20// This extends to data movement: `blit_copy` copies **zero bytes**. It
21// records a `MetalCommand::BlitCommand` on a queued command buffer, commits
22// and "waits for" that buffer (both of which complete synchronously and
23// never dereference `src`/`dst`), and increments `MetalStats::blit_commands`
24// — that counter says "this many `blit_copy` calls were made," not "this
25// many bytes moved." An earlier revision of
26// `commit_command_buffer`/`wait_until_completed`/`wait_until_idle` also
27// injected a `std::thread::sleep` to imitate command-buffer execution
28// latency; that fake timing has been removed, so every command buffer now
29// completes the instant it is committed. `MetalStats::compute_commands`,
30// `MetalStats::render_commands` and `MetalStats::command_buffers_completed`
31// are declared for API-shape completeness but nothing in this module ever
32// increments them — read a `0` there as "not tracked," not "none occurred."
33
34use std::collections::HashMap;
35use std::ffi::c_void;
36use std::sync::{Arc, Mutex};
37use std::time::{Duration, Instant};
38
39/// Upper bound this module's simulated allocator supports for a caller-
40/// requested alignment (this module only ever requests 16 or 64). Also the
41/// fixed header size reserved ahead of every non-empty allocation, so a
42/// request with alignment `<= SIM_ALLOC_ALIGN` needs no per-call alignment
43/// bookkeeping: over-aligning to `SIM_ALLOC_ALIGN` always satisfies a
44/// smaller request too.
45const SIM_ALLOC_ALIGN: usize = 256;
46
47/// Allocate `size` bytes at (at least) `align`-byte alignment through the
48/// system allocator, without the two ways the naive
49/// `std::alloc::alloc(Layout::from_size_align_unchecked(size, align))` this
50/// module used to call was undefined behaviour: a zero-size layout is
51/// unsound to pass to `GlobalAlloc::alloc`, and a real allocation failure
52/// returns null, which must never be treated as valid memory. See
53/// `cuda_backend::sim_alloc` for the full rationale (this mirrors it, with
54/// an added alignment check since this module's alignment is caller-chosen
55/// rather than a fixed constant).
56///
57/// The payload is prefixed with a `SIM_ALLOC_ALIGN`-byte header recording
58/// the requested size, so [`sim_dealloc`] can reconstruct the exact `Layout`
59/// this function used — the `Layout::from_size_align_unchecked(1, 1)` this
60/// module used at free time was a mismatched-layout deallocation, itself
61/// unconditionally undefined behaviour.
62fn sim_alloc(size: usize, align: usize) -> Result<*mut c_void, MetalError> {
63    if !align.is_power_of_two() || align > SIM_ALLOC_ALIGN {
64        return Err(MetalError::AllocationFailed(format!(
65            "unsupported allocation alignment {align}: this simulated backend supports \
66             power-of-two alignments up to {SIM_ALLOC_ALIGN} bytes"
67        )));
68    }
69    if size == 0 {
70        return Ok(SIM_ALLOC_ALIGN as *mut c_void);
71    }
72    let total = SIM_ALLOC_ALIGN.checked_add(size).ok_or_else(|| {
73        MetalError::AllocationFailed(format!(
74            "{size}-byte request overflows the allocator's size limit"
75        ))
76    })?;
77    // Always allocate at `SIM_ALLOC_ALIGN`: since `align <= SIM_ALLOC_ALIGN`
78    // and both are powers of two, this over-aligned block also satisfies the
79    // caller's smaller request.
80    let layout = std::alloc::Layout::from_size_align(total, SIM_ALLOC_ALIGN)
81        .map_err(|e| MetalError::AllocationFailed(format!("invalid allocation layout: {e}")))?;
82    // SAFETY: `layout` has non-zero size (checked above) and a valid
83    // (power-of-two) alignment constructed by `Layout::from_size_align`.
84    let base = unsafe { std::alloc::alloc(layout) };
85    if base.is_null() {
86        return Err(MetalError::AllocationFailed(format!(
87            "allocator returned null for a {size}-byte request"
88        )));
89    }
90    // SAFETY: `base` is non-null and `layout`'s size is at least
91    // `SIM_ALLOC_ALIGN + size >= SIM_ALLOC_ALIGN >= size_of::<usize>()`, so
92    // writing one `usize` at the start of the block is in-bounds.
93    unsafe { (base as *mut usize).write(size) };
94    // SAFETY: `base` was allocated with `total = SIM_ALLOC_ALIGN + size`
95    // bytes, so offsetting by `SIM_ALLOC_ALIGN` stays within (or one past)
96    // the allocation.
97    Ok(unsafe { base.add(SIM_ALLOC_ALIGN) } as *mut c_void)
98}
99
100/// Free a pointer returned by [`sim_alloc`]. A no-op for a null pointer or
101/// the zero-size sentinel — neither was ever allocated.
102///
103/// Only ever called from this module (it is not `pub`) with a pointer
104/// `sim_alloc` returned that has not already been freed — the unsafety of
105/// the pointer arithmetic below is contained to that invariant, matching
106/// this module's existing style of confining `unsafe` to the raw
107/// `std::alloc` calls rather than marking `free()`'s public wrapper unsafe.
108fn sim_dealloc(ptr: *mut c_void) {
109    if ptr.is_null() || (ptr as usize) == SIM_ALLOC_ALIGN {
110        return;
111    }
112    // SAFETY: by this function's contract `ptr` came from `sim_alloc`.
113    let base = unsafe { (ptr as *mut u8).sub(SIM_ALLOC_ALIGN) };
114    // SAFETY: `sim_alloc` wrote a `usize` at `base` before returning.
115    let size = unsafe { (base as *const usize).read() };
116    if let Ok(layout) = std::alloc::Layout::from_size_align(SIM_ALLOC_ALIGN + size, SIM_ALLOC_ALIGN)
117    {
118        // SAFETY: `layout` is exactly the layout `sim_alloc` allocated
119        // `base` with.
120        unsafe { std::alloc::dealloc(base, layout) };
121    }
122}
123
124/// Metal memory backend implementation
125pub struct MetalMemoryBackend {
126    /// Backend configuration
127    config: MetalConfig,
128    /// Device properties
129    device_properties: MetalDeviceProperties,
130    /// Memory pools
131    memory_pools: HashMap<MetalMemoryType, MetalMemoryPool>,
132    /// Statistics
133    stats: MetalStats,
134    /// Command queue management
135    command_manager: MetalCommandManager,
136}
137
138/// Metal backend configuration
139#[derive(Debug, Clone)]
140pub struct MetalConfig {
141    /// Device ID to use
142    pub device_id: u32,
143    /// Enable private memory
144    pub enable_private_memory: bool,
145    /// Enable shared memory
146    pub enable_shared_memory: bool,
147    /// Enable managed memory
148    pub enable_managed_memory: bool,
149    /// Enable memory pools
150    pub enable_memory_pools: bool,
151    /// Enable async memory operations
152    pub enable_async_ops: bool,
153    /// Memory pool growth size
154    pub pool_growth_size: usize,
155    /// Enable memoryless render targets
156    pub enable_memoryless_targets: bool,
157    /// Maximum number of command queues
158    pub max_command_queues: u32,
159    /// Enable Metal Performance Shaders
160    pub enable_mps: bool,
161    /// Enable heap-based allocation
162    pub enable_heap_allocation: bool,
163}
164
165impl Default for MetalConfig {
166    fn default() -> Self {
167        Self {
168            device_id: 0,
169            enable_private_memory: true,
170            enable_shared_memory: true,
171            enable_managed_memory: true,
172            enable_memory_pools: true,
173            enable_async_ops: true,
174            pool_growth_size: 64 * 1024 * 1024, // 64MB
175            enable_memoryless_targets: false,
176            max_command_queues: 8,
177            enable_mps: true,
178            enable_heap_allocation: true,
179        }
180    }
181}
182
183/// Metal device properties
184#[derive(Debug, Clone)]
185pub struct MetalDeviceProperties {
186    pub device_id: u32,
187    pub name: String,
188    pub device_type: MetalDeviceType,
189    pub family: MetalGPUFamily,
190    pub max_threads_per_threadgroup: u32,
191    pub threadgroup_memory_length: u32,
192    pub max_buffer_length: usize,
193    pub max_texture_size_2d: u32,
194    pub max_texture_size_3d: u32,
195    pub unified_memory: bool,
196    pub discrete_memory: bool,
197    pub low_power: bool,
198    pub headless: bool,
199    pub supports_shader_debugging: bool,
200    pub supports_function_pointers: bool,
201    pub supports_dynamic_libraries: bool,
202    pub supports_render_dynamic_libraries: bool,
203    pub recommended_max_working_set_size: usize,
204    pub max_transfer_rate: u64,
205    pub has_unified_memory: bool,
206}
207
208/// Metal device types
209#[derive(Debug, Clone, PartialEq)]
210pub enum MetalDeviceType {
211    Integrated,
212    Discrete,
213    External,
214    Virtual,
215}
216
217/// Metal GPU families (Apple Silicon generations)
218#[derive(Debug, Clone, PartialEq)]
219pub enum MetalGPUFamily {
220    Apple1, // A7
221    Apple2, // A8
222    Apple3, // A9, A10
223    Apple4, // A11
224    Apple5, // A12, A13
225    Apple6, // A14, M1
226    Apple7, // A15, M1 Pro, M1 Max
227    Apple8, // A16, M2
228    Apple9, // M2 Pro, M2 Max, M3
229    Mac1,   // Intel Iris Pro
230    Mac2,   // Intel Iris Pro, AMD
231}
232
233/// Metal memory types
234#[derive(Debug, Clone, PartialEq, Eq, Hash)]
235pub enum MetalMemoryType {
236    Private,    // GPU-only memory
237    Shared,     // CPU-GPU shared memory
238    Managed,    // Automatically managed memory
239    Memoryless, // Tile memory (iOS only)
240}
241
242/// Metal device abstraction
243pub struct MetalDevice {
244    /// Device handle (simulated)
245    pub handle: *mut c_void,
246    /// Device ID
247    pub device_id: u32,
248    /// Device properties
249    pub properties: MetalDeviceProperties,
250    /// Creation time
251    pub created_at: Instant,
252    /// Command queues
253    pub command_queues: Vec<MetalCommandQueue>,
254    /// Memory heaps
255    pub heaps: HashMap<usize, MetalHeap>,
256    /// Active resources
257    pub resources: HashMap<*mut c_void, MetalResource>,
258}
259
260/// Metal command queue for GPU operations
261pub struct MetalCommandQueue {
262    /// Queue handle (simulated)
263    pub handle: *mut c_void,
264    /// Queue ID
265    pub id: u32,
266    /// Queue label
267    pub label: Option<String>,
268    /// Creation time
269    pub created_at: Instant,
270    /// Command buffers
271    pub command_buffers: std::collections::VecDeque<MetalCommandBuffer>,
272    /// Queue priority
273    pub priority: MetalQueuePriority,
274}
275
276/// Metal queue priorities
277#[derive(Debug, Clone, PartialEq)]
278pub enum MetalQueuePriority {
279    High,
280    Normal,
281    Low,
282    Background,
283}
284
285/// Metal command buffer
286#[derive(Debug, Clone)]
287pub struct MetalCommandBuffer {
288    pub buffer_id: u32,
289    pub commands: Vec<MetalCommand>,
290    pub timestamp: Instant,
291    pub committed: bool,
292    pub completed: bool,
293}
294
295/// Metal GPU commands
296#[derive(Debug, Clone)]
297pub enum MetalCommand {
298    BlitCommand {
299        src_buffer: *mut c_void,
300        dst_buffer: *mut c_void,
301        size: usize,
302    },
303    ComputeCommand {
304        kernel_id: u32,
305        threadgroup_size: (u32, u32, u32),
306        threadgroups: (u32, u32, u32),
307    },
308    RenderCommand {
309        render_pass: u32,
310    },
311    MemoryBarrier,
312}
313
314/// Metal memory pool
315pub struct MetalMemoryPool {
316    /// Memory type
317    memory_type: MetalMemoryType,
318    /// Current size
319    current_size: usize,
320    /// Maximum size
321    max_size: usize,
322    /// Used size
323    used_size: usize,
324    /// Free blocks
325    free_blocks: std::collections::VecDeque<MetalMemoryBlock>,
326    /// Allocated blocks
327    allocated_blocks: HashMap<*mut c_void, MetalMemoryBlock>,
328    /// Storage mode
329    storage_mode: MetalStorageMode,
330    /// Cache mode
331    cache_mode: MetalCacheMode,
332}
333
334/// Metal memory block
335#[derive(Debug, Clone)]
336pub struct MetalMemoryBlock {
337    pub ptr: *mut c_void,
338    pub size: usize,
339    pub memory_type: MetalMemoryType,
340    pub allocated_at: Instant,
341    pub last_access: Option<Instant>,
342    pub ref_count: u32,
343    pub storage_mode: MetalStorageMode,
344    pub cache_mode: MetalCacheMode,
345    pub gpu_address: Option<u64>,
346}
347
348/// Metal storage modes
349#[derive(Debug, Clone, PartialEq)]
350pub enum MetalStorageMode {
351    Shared,     // CPU and GPU accessible
352    Managed,    // Managed by Metal
353    Private,    // GPU-only
354    Memoryless, // Tile memory
355}
356
357/// Metal cache modes
358#[derive(Debug, Clone, PartialEq)]
359pub enum MetalCacheMode {
360    DefaultCache,
361    WriteCombined,
362}
363
364/// Metal heap for resource allocation
365pub struct MetalHeap {
366    /// Heap handle (simulated)
367    pub handle: *mut c_void,
368    /// Heap ID
369    pub id: usize,
370    /// Size
371    pub size: usize,
372    /// Used size
373    pub used_size: usize,
374    /// Storage mode
375    pub storage_mode: MetalStorageMode,
376    /// CPU cache mode
377    pub cpu_cache_mode: MetalCacheMode,
378    /// Allocated resources
379    pub resources: HashMap<*mut c_void, MetalResource>,
380}
381
382/// Metal resource (buffer, texture, etc.)
383#[derive(Debug, Clone)]
384pub struct MetalResource {
385    pub ptr: *mut c_void,
386    pub size: usize,
387    pub resource_type: MetalResourceType,
388    pub storage_mode: MetalStorageMode,
389    pub allocated_at: Instant,
390    pub heap_offset: Option<usize>,
391}
392
393/// Metal resource types
394#[derive(Debug, Clone, PartialEq)]
395pub enum MetalResourceType {
396    Buffer,
397    Texture1D,
398    Texture2D,
399    Texture3D,
400    TextureCube,
401}
402
403impl MetalMemoryPool {
404    pub fn new(memory_type: MetalMemoryType, max_size: usize) -> Self {
405        let (storage_mode, cache_mode) = match memory_type {
406            MetalMemoryType::Private => (MetalStorageMode::Private, MetalCacheMode::DefaultCache),
407            MetalMemoryType::Shared => (MetalStorageMode::Shared, MetalCacheMode::DefaultCache),
408            MetalMemoryType::Managed => (MetalStorageMode::Managed, MetalCacheMode::DefaultCache),
409            MetalMemoryType::Memoryless => {
410                (MetalStorageMode::Memoryless, MetalCacheMode::DefaultCache)
411            }
412        };
413
414        Self {
415            memory_type,
416            current_size: 0,
417            max_size,
418            used_size: 0,
419            free_blocks: std::collections::VecDeque::new(),
420            allocated_blocks: HashMap::new(),
421            storage_mode,
422            cache_mode,
423        }
424    }
425
426    /// Allocate from pool
427    pub fn allocate(&mut self, size: usize) -> Result<*mut c_void, MetalError> {
428        // Try to find suitable free block
429        for i in 0..self.free_blocks.len() {
430            if self.free_blocks[i].size >= size {
431                let Some(mut block) = self.free_blocks.remove(i) else {
432                    continue;
433                };
434
435                // Split block if much larger
436                if block.size > size * 2 {
437                    let remaining_block = MetalMemoryBlock {
438                        ptr: unsafe { block.ptr.add(size) },
439                        size: block.size - size,
440                        memory_type: block.memory_type.clone(),
441                        allocated_at: block.allocated_at,
442                        last_access: None,
443                        ref_count: 0,
444                        storage_mode: block.storage_mode.clone(),
445                        cache_mode: block.cache_mode.clone(),
446                        gpu_address: None,
447                    };
448                    self.free_blocks.push_back(remaining_block);
449                    block.size = size;
450                }
451
452                block.last_access = Some(Instant::now());
453                block.ref_count = 1;
454
455                let ptr = block.ptr;
456                self.allocated_blocks.insert(ptr, block);
457                self.used_size += size;
458
459                return Ok(ptr);
460            }
461        }
462
463        // Need to allocate new memory
464        if self.current_size + size > self.max_size {
465            return Err(MetalError::OutOfMemory(
466                "Pool size limit exceeded".to_string(),
467            ));
468        }
469
470        let ptr = self.metal_allocate(size)?;
471        let block = MetalMemoryBlock {
472            ptr,
473            size,
474            memory_type: self.memory_type.clone(),
475            allocated_at: Instant::now(),
476            last_access: Some(Instant::now()),
477            ref_count: 1,
478            storage_mode: self.storage_mode.clone(),
479            cache_mode: self.cache_mode.clone(),
480            gpu_address: Some(ptr as u64), // Simulate GPU address
481        };
482
483        self.allocated_blocks.insert(ptr, block);
484        self.current_size += size;
485        self.used_size += size;
486
487        Ok(ptr)
488    }
489
490    /// Free back to pool
491    pub fn free(&mut self, ptr: *mut c_void) -> Result<(), MetalError> {
492        if let Some(block) = self.allocated_blocks.remove(&ptr) {
493            self.used_size -= block.size;
494
495            // Add to free blocks
496            self.free_blocks.push_back(MetalMemoryBlock {
497                ptr: block.ptr,
498                size: block.size,
499                memory_type: block.memory_type,
500                allocated_at: block.allocated_at,
501                last_access: None,
502                ref_count: 0,
503                storage_mode: block.storage_mode,
504                cache_mode: block.cache_mode,
505                gpu_address: block.gpu_address,
506            });
507
508            // Try to coalesce adjacent blocks
509            self.coalesce_free_blocks();
510
511            Ok(())
512        } else {
513            Err(MetalError::InvalidPointer(
514                "Pointer not found in pool".to_string(),
515            ))
516        }
517    }
518
519    fn coalesce_free_blocks(&mut self) {
520        // Sort free blocks by address
521        let mut blocks: Vec<MetalMemoryBlock> = self.free_blocks.drain(..).collect();
522        blocks.sort_by_key(|block| block.ptr as usize);
523
524        let mut coalesced = Vec::new();
525        let mut current_block: Option<MetalMemoryBlock> = None;
526
527        for block in blocks {
528            match current_block.take() {
529                None => current_block = Some(block),
530                Some(mut prev_block) => {
531                    let prev_end = prev_block.ptr as usize + prev_block.size;
532                    let block_start = block.ptr as usize;
533
534                    if prev_end == block_start && prev_block.memory_type == block.memory_type {
535                        // Coalesce blocks
536                        prev_block.size += block.size;
537                        current_block = Some(prev_block);
538                    } else {
539                        coalesced.push(prev_block);
540                        current_block = Some(block);
541                    }
542                }
543            }
544        }
545
546        if let Some(block) = current_block {
547            coalesced.push(block);
548        }
549
550        self.free_blocks = coalesced.into();
551    }
552
553    fn metal_allocate(&self, size: usize) -> Result<*mut c_void, MetalError> {
554        // Simulate Metal buffer allocation
555        let alignment = match self.memory_type {
556            MetalMemoryType::Private => 64,    // GPU alignment
557            MetalMemoryType::Shared => 16,     // CPU-GPU shared
558            MetalMemoryType::Managed => 16,    // Managed memory
559            MetalMemoryType::Memoryless => 64, // Tile memory
560        };
561
562        match self.memory_type {
563            MetalMemoryType::Private => sim_alloc(size, alignment), // MTLBuffer, private storage
564            MetalMemoryType::Shared => sim_alloc(size, alignment),  // MTLBuffer, shared storage
565            MetalMemoryType::Managed => sim_alloc(size, alignment), // MTLBuffer, managed storage
566            MetalMemoryType::Memoryless => {
567                // Memoryless render target (tile memory)
568                if size > 8 * 1024 * 1024 {
569                    // 8MB tile memory limit
570                    return Err(MetalError::UnsupportedOperation(
571                        "Memoryless allocation too large".to_string(),
572                    ));
573                }
574                sim_alloc(size, alignment)
575            }
576        }
577    }
578}
579
580/// Metal command manager
581pub struct MetalCommandManager {
582    /// Command queues
583    queues: Vec<MetalCommandQueue>,
584    /// Next queue ID
585    next_queue_id: u32,
586    /// Next command buffer ID
587    next_buffer_id: u32,
588    /// Configuration
589    config: MetalCommandConfig,
590}
591
592/// Command manager configuration
593#[derive(Debug, Clone)]
594pub struct MetalCommandConfig {
595    pub max_command_buffers_per_queue: usize,
596    pub enable_command_buffer_reuse: bool,
597    pub enable_parallel_encoding: bool,
598}
599
600impl Default for MetalCommandConfig {
601    fn default() -> Self {
602        Self {
603            max_command_buffers_per_queue: 64,
604            enable_command_buffer_reuse: true,
605            enable_parallel_encoding: true,
606        }
607    }
608}
609
610impl MetalCommandManager {
611    pub fn new(config: MetalCommandConfig) -> Self {
612        Self {
613            queues: Vec::new(),
614            next_queue_id: 0,
615            next_buffer_id: 0,
616            config,
617        }
618    }
619
620    /// Create command queue
621    pub fn create_command_queue(
622        &mut self,
623        label: Option<String>,
624        priority: MetalQueuePriority,
625    ) -> Result<u32, MetalError> {
626        let queue_id = self.next_queue_id;
627        self.next_queue_id += 1;
628
629        let queue = MetalCommandQueue {
630            handle: std::ptr::null_mut(),
631            id: queue_id,
632            label,
633            created_at: Instant::now(),
634            command_buffers: std::collections::VecDeque::new(),
635            priority,
636        };
637
638        self.queues.push(queue);
639        Ok(queue_id)
640    }
641
642    /// Create command buffer
643    pub fn create_command_buffer(&mut self, queue_id: u32) -> Result<u32, MetalError> {
644        if let Some(queue) = self.queues.iter_mut().find(|q| q.id == queue_id) {
645            if queue.command_buffers.len() >= self.config.max_command_buffers_per_queue {
646                return Err(MetalError::QueueFull("Command queue is full".to_string()));
647            }
648
649            let buffer_id = self.next_buffer_id;
650            self.next_buffer_id += 1;
651
652            let command_buffer = MetalCommandBuffer {
653                buffer_id,
654                commands: Vec::new(),
655                timestamp: Instant::now(),
656                committed: false,
657                completed: false,
658            };
659
660            queue.command_buffers.push_back(command_buffer);
661            Ok(buffer_id)
662        } else {
663            Err(MetalError::InvalidQueue("Queue not found".to_string()))
664        }
665    }
666
667    /// Add command to buffer
668    pub fn add_command(
669        &mut self,
670        queue_id: u32,
671        buffer_id: u32,
672        command: MetalCommand,
673    ) -> Result<(), MetalError> {
674        if let Some(queue) = self.queues.iter_mut().find(|q| q.id == queue_id) {
675            if let Some(buffer) = queue
676                .command_buffers
677                .iter_mut()
678                .find(|b| b.buffer_id == buffer_id)
679            {
680                if buffer.committed {
681                    return Err(MetalError::InvalidOperation(
682                        "Command buffer already committed".to_string(),
683                    ));
684                }
685                buffer.commands.push(command);
686                Ok(())
687            } else {
688                Err(MetalError::InvalidCommandBuffer(
689                    "Command buffer not found".to_string(),
690                ))
691            }
692        } else {
693            Err(MetalError::InvalidQueue("Queue not found".to_string()))
694        }
695    }
696
697    /// Commit command buffer
698    pub fn commit_command_buffer(
699        &mut self,
700        queue_id: u32,
701        buffer_id: u32,
702    ) -> Result<(), MetalError> {
703        if let Some(queue) = self.queues.iter_mut().find(|q| q.id == queue_id) {
704            if let Some(buffer) = queue
705                .command_buffers
706                .iter_mut()
707                .find(|b| b.buffer_id == buffer_id)
708            {
709                buffer.committed = true;
710                // Host-memory simulation (see module docs): there is no real
711                // Metal command queue to submit to, so this used to inject an
712                // artificial `std::thread::sleep` to mimic command-buffer
713                // execution latency before marking the buffer complete. That
714                // fake timing has been removed; the buffer now completes
715                // immediately rather than after a fabricated delay.
716                buffer.completed = true;
717                Ok(())
718            } else {
719                Err(MetalError::InvalidCommandBuffer(
720                    "Command buffer not found".to_string(),
721                ))
722            }
723        } else {
724            Err(MetalError::InvalidQueue("Queue not found".to_string()))
725        }
726    }
727
728    /// Wait for completion
729    pub fn wait_until_completed(
730        &mut self,
731        queue_id: u32,
732        buffer_id: u32,
733    ) -> Result<(), MetalError> {
734        if let Some(queue) = self.queues.iter().find(|q| q.id == queue_id) {
735            if queue
736                .command_buffers
737                .iter()
738                .any(|b| b.buffer_id == buffer_id)
739            {
740                // Host-memory simulation (see module docs): there is no real
741                // Metal command queue to poll, and `commit_command_buffer`
742                // already completes every buffer synchronously, so there is
743                // nothing left to wait for here. This used to inject an
744                // artificial `std::thread::sleep` in the not-yet-completed
745                // case to mimic polling latency; that fake timing has been
746                // removed.
747                Ok(())
748            } else {
749                Err(MetalError::InvalidCommandBuffer(
750                    "Command buffer not found".to_string(),
751                ))
752            }
753        } else {
754            Err(MetalError::InvalidQueue("Queue not found".to_string()))
755        }
756    }
757}
758
759/// Metal statistics
760#[derive(Debug, Clone, Default)]
761pub struct MetalStats {
762    pub total_allocations: u64,
763    pub total_deallocations: u64,
764    pub bytes_allocated: u64,
765    pub bytes_deallocated: u64,
766    pub private_memory_used: usize,
767    pub shared_memory_used: usize,
768    pub managed_memory_used: usize,
769    pub command_buffers_created: u64,
770    pub command_buffers_completed: u64,
771    pub compute_commands: u64,
772    pub blit_commands: u64,
773    pub render_commands: u64,
774    pub average_allocation_time: Duration,
775    pub peak_memory_usage: usize,
776}
777
778impl MetalMemoryBackend {
779    /// Create new Metal backend
780    pub fn new(config: MetalConfig) -> Result<Self, MetalError> {
781        // Query Metal device
782        let device_properties = Self::query_device_properties(config.device_id)?;
783
784        // Create memory pools
785        let mut memory_pools = HashMap::new();
786        if config.enable_memory_pools {
787            let pool_size = device_properties.recommended_max_working_set_size / 4;
788
789            if config.enable_private_memory {
790                memory_pools.insert(
791                    MetalMemoryType::Private,
792                    MetalMemoryPool::new(MetalMemoryType::Private, pool_size),
793                );
794            }
795
796            if config.enable_shared_memory {
797                memory_pools.insert(
798                    MetalMemoryType::Shared,
799                    MetalMemoryPool::new(MetalMemoryType::Shared, pool_size),
800                );
801            }
802
803            if config.enable_managed_memory {
804                memory_pools.insert(
805                    MetalMemoryType::Managed,
806                    MetalMemoryPool::new(MetalMemoryType::Managed, pool_size),
807                );
808            }
809        }
810
811        let command_manager = MetalCommandManager::new(MetalCommandConfig::default());
812
813        Ok(Self {
814            config,
815            device_properties,
816            memory_pools,
817            stats: MetalStats::default(),
818            command_manager,
819        })
820    }
821
822    /// Query device properties
823    fn query_device_properties(device_id: u32) -> Result<MetalDeviceProperties, MetalError> {
824        // Simulate querying Metal device properties
825        Ok(MetalDeviceProperties {
826            device_id,
827            name: "Apple M1 Pro".to_string(),
828            device_type: MetalDeviceType::Integrated,
829            family: MetalGPUFamily::Apple7,
830            max_threads_per_threadgroup: 1024,
831            threadgroup_memory_length: 32768,
832            max_buffer_length: 2 * 1024 * 1024 * 1024, // 2GB
833            max_texture_size_2d: 16384,
834            max_texture_size_3d: 2048,
835            unified_memory: true,
836            discrete_memory: false,
837            low_power: false,
838            headless: false,
839            supports_shader_debugging: true,
840            supports_function_pointers: true,
841            supports_dynamic_libraries: true,
842            supports_render_dynamic_libraries: true,
843            recommended_max_working_set_size: 32 * 1024 * 1024 * 1024, // 32GB
844            max_transfer_rate: 400_000_000_000,                        // 400 GB/s
845            has_unified_memory: true,
846        })
847    }
848
849    /// Allocate memory
850    pub fn allocate(
851        &mut self,
852        size: usize,
853        memory_type: MetalMemoryType,
854    ) -> Result<*mut c_void, MetalError> {
855        let start_time = Instant::now();
856
857        let ptr = if self.config.enable_memory_pools {
858            if let Some(pool) = self.memory_pools.get_mut(&memory_type) {
859                pool.allocate(size)?
860            } else {
861                return Err(MetalError::UnsupportedMemoryType(
862                    "Memory type not supported".to_string(),
863                ));
864            }
865        } else {
866            // Direct allocation
867            self.direct_allocate(size, memory_type.clone())?
868        };
869
870        // Update statistics
871        self.stats.total_allocations += 1;
872        self.stats.bytes_allocated += size as u64;
873
874        match memory_type {
875            MetalMemoryType::Private => self.stats.private_memory_used += size,
876            MetalMemoryType::Shared => self.stats.shared_memory_used += size,
877            MetalMemoryType::Managed => self.stats.managed_memory_used += size,
878            _ => {}
879        }
880
881        let allocation_time = start_time.elapsed();
882        let total_time = self.stats.average_allocation_time.as_nanos() as u64
883            * (self.stats.total_allocations - 1)
884            + allocation_time.as_nanos() as u64;
885        self.stats.average_allocation_time =
886            Duration::from_nanos(total_time / self.stats.total_allocations);
887
888        let current_usage = self.stats.private_memory_used
889            + self.stats.shared_memory_used
890            + self.stats.managed_memory_used;
891        if current_usage > self.stats.peak_memory_usage {
892            self.stats.peak_memory_usage = current_usage;
893        }
894
895        Ok(ptr)
896    }
897
898    fn direct_allocate(
899        &self,
900        size: usize,
901        memory_type: MetalMemoryType,
902    ) -> Result<*mut c_void, MetalError> {
903        let alignment = match memory_type {
904            MetalMemoryType::Private => 64,
905            MetalMemoryType::Shared => 16,
906            MetalMemoryType::Managed => 16,
907            MetalMemoryType::Memoryless => 64,
908        };
909
910        // Simulate Metal buffer allocation
911        match memory_type {
912            MetalMemoryType::Private => sim_alloc(size, alignment),
913            MetalMemoryType::Shared => sim_alloc(size, alignment),
914            MetalMemoryType::Managed => sim_alloc(size, alignment),
915            MetalMemoryType::Memoryless => {
916                if size > 8 * 1024 * 1024 {
917                    return Err(MetalError::UnsupportedOperation(
918                        "Memoryless allocation too large".to_string(),
919                    ));
920                }
921                sim_alloc(size, alignment)
922            }
923        }
924    }
925
926    /// Free memory
927    pub fn free(
928        &mut self,
929        ptr: *mut c_void,
930        memory_type: MetalMemoryType,
931    ) -> Result<(), MetalError> {
932        if self.config.enable_memory_pools {
933            if let Some(pool) = self.memory_pools.get_mut(&memory_type) {
934                pool.free(ptr)?;
935            } else {
936                return Err(MetalError::UnsupportedMemoryType(
937                    "Memory type not supported".to_string(),
938                ));
939            }
940        } else {
941            // Direct deallocation. `ptr` was returned by `sim_alloc` via
942            // the allocation methods above, and this is the first time it
943            // is freed.
944            sim_dealloc(ptr);
945        }
946
947        self.stats.total_deallocations += 1;
948        Ok(())
949    }
950
951    /// Copy memory using Metal blit encoder
952    pub fn blit_copy(
953        &mut self,
954        src: *const c_void,
955        dst: *mut c_void,
956        size: usize,
957        queue_id: u32,
958    ) -> Result<(), MetalError> {
959        let buffer_id = self.command_manager.create_command_buffer(queue_id)?;
960        let command = MetalCommand::BlitCommand {
961            src_buffer: src as *mut c_void,
962            dst_buffer: dst,
963            size,
964        };
965
966        self.command_manager
967            .add_command(queue_id, buffer_id, command)?;
968        self.command_manager
969            .commit_command_buffer(queue_id, buffer_id)?;
970        self.command_manager
971            .wait_until_completed(queue_id, buffer_id)?;
972
973        self.stats.blit_commands += 1;
974        Ok(())
975    }
976
977    /// Create command queue
978    pub fn create_command_queue(
979        &mut self,
980        label: Option<String>,
981        priority: MetalQueuePriority,
982    ) -> Result<u32, MetalError> {
983        self.command_manager.create_command_queue(label, priority)
984    }
985
986    /// Get device properties
987    pub fn get_device_properties(&self) -> &MetalDeviceProperties {
988        &self.device_properties
989    }
990
991    /// Get statistics
992    pub fn get_stats(&self) -> &MetalStats {
993        &self.stats
994    }
995
996    /// Wait for all operations to complete
997    pub fn wait_until_idle(&mut self) -> Result<(), MetalError> {
998        // Host-memory simulation (see module docs): `commit_command_buffer`
999        // completes every buffer synchronously, so there is never a
1000        // committed-but-incomplete buffer to wait on here. This used to walk
1001        // every queue's command buffers and inject an artificial
1002        // `std::thread::sleep` for any it found in that (unreachable) state;
1003        // that fake timing has been removed along with the now-vestigial
1004        // scan, since it never changed the `Ok(())` result below.
1005        Ok(())
1006    }
1007}
1008
1009// Safety: MetalMemoryBackend manages Metal GPU memory pointers via *mut c_void.
1010// While raw pointers are not Send/Sync by default, it's safe to share across threads
1011// when protected by Arc<Mutex<>> because:
1012// 1. All pointers point to Metal GPU memory managed by the Metal framework
1013// 2. The Mutex provides exclusive access for all mutable operations
1014// 3. No thread-local state is maintained
1015unsafe impl Send for MetalMemoryBackend {}
1016unsafe impl Sync for MetalMemoryBackend {}
1017
1018/// Metal errors
1019#[derive(Debug, Clone)]
1020pub enum MetalError {
1021    DeviceNotFound(String),
1022    OutOfMemory(String),
1023    InvalidPointer(String),
1024    InvalidQueue(String),
1025    InvalidCommandBuffer(String),
1026    QueueFull(String),
1027    InvalidOperation(String),
1028    UnsupportedOperation(String),
1029    UnsupportedMemoryType(String),
1030    AllocationFailed(String),
1031    InternalError(String),
1032}
1033
1034impl std::fmt::Display for MetalError {
1035    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1036        match self {
1037            MetalError::DeviceNotFound(msg) => write!(f, "Device not found: {}", msg),
1038            MetalError::OutOfMemory(msg) => write!(f, "Out of memory: {}", msg),
1039            MetalError::InvalidPointer(msg) => write!(f, "Invalid pointer: {}", msg),
1040            MetalError::InvalidQueue(msg) => write!(f, "Invalid queue: {}", msg),
1041            MetalError::InvalidCommandBuffer(msg) => write!(f, "Invalid command buffer: {}", msg),
1042            MetalError::QueueFull(msg) => write!(f, "Queue full: {}", msg),
1043            MetalError::InvalidOperation(msg) => write!(f, "Invalid operation: {}", msg),
1044            MetalError::UnsupportedOperation(msg) => write!(f, "Unsupported operation: {}", msg),
1045            MetalError::UnsupportedMemoryType(msg) => write!(f, "Unsupported memory type: {}", msg),
1046            MetalError::AllocationFailed(msg) => write!(f, "Allocation failed: {}", msg),
1047            MetalError::InternalError(msg) => write!(f, "Internal error: {}", msg),
1048        }
1049    }
1050}
1051
1052impl std::error::Error for MetalError {}
1053
1054/// Thread-safe Metal backend wrapper
1055pub struct ThreadSafeMetalBackend {
1056    backend: Arc<Mutex<MetalMemoryBackend>>,
1057}
1058
1059impl ThreadSafeMetalBackend {
1060    pub fn new(config: MetalConfig) -> Result<Self, MetalError> {
1061        let backend = MetalMemoryBackend::new(config)?;
1062        Ok(Self {
1063            backend: Arc::new(Mutex::new(backend)),
1064        })
1065    }
1066
1067    pub fn allocate(
1068        &self,
1069        size: usize,
1070        memory_type: MetalMemoryType,
1071    ) -> Result<*mut c_void, MetalError> {
1072        let mut backend = self.backend.lock().unwrap_or_else(|e| e.into_inner());
1073        backend.allocate(size, memory_type)
1074    }
1075
1076    pub fn free(&self, ptr: *mut c_void, memory_type: MetalMemoryType) -> Result<(), MetalError> {
1077        let mut backend = self.backend.lock().unwrap_or_else(|e| e.into_inner());
1078        backend.free(ptr, memory_type)
1079    }
1080
1081    pub fn get_stats(&self) -> MetalStats {
1082        let backend = self.backend.lock().unwrap_or_else(|e| e.into_inner());
1083        backend.get_stats().clone()
1084    }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090
1091    /// Regression test for F26: a zero-size request must not reach
1092    /// `std::alloc::alloc` (unsound for a zero-size layout), and an
1093    /// out-of-range alignment must be an honest error, not silent UB.
1094    #[test]
1095    fn sim_alloc_zero_size_is_a_safe_sentinel_not_a_ub_call() {
1096        let ptr = sim_alloc(0, 64).expect("zero-size request must succeed");
1097        assert!(!ptr.is_null());
1098        sim_dealloc(ptr);
1099    }
1100
1101    #[test]
1102    fn sim_alloc_rejects_unsupported_alignment() {
1103        assert!(sim_alloc(16, 3).is_err(), "3 is not a power of two");
1104        assert!(
1105            sim_alloc(16, 512).is_err(),
1106            "512 exceeds this simulated backend's supported alignment"
1107        );
1108    }
1109
1110    /// A real allocation must be readable/writable across its full size and
1111    /// must free through the same layout it was allocated with.
1112    #[test]
1113    fn sim_alloc_real_allocation_round_trips_and_frees_cleanly() {
1114        for (size, align) in [(1usize, 16usize), (7, 16), (256, 64), (4096, 64)] {
1115            let ptr = sim_alloc(size, align).expect("allocation must succeed") as *mut u8;
1116            assert!(!ptr.is_null());
1117            assert_eq!(
1118                (ptr as usize) % align,
1119                0,
1120                "returned pointer does not honour the requested alignment"
1121            );
1122            unsafe {
1123                for i in 0..size {
1124                    ptr.add(i).write(0xAB);
1125                }
1126                for i in 0..size {
1127                    assert_eq!(ptr.add(i).read(), 0xAB);
1128                }
1129                sim_dealloc(ptr as *mut c_void);
1130            }
1131        }
1132    }
1133
1134    #[test]
1135    fn sim_dealloc_null_is_a_no_op() {
1136        sim_dealloc(std::ptr::null_mut());
1137    }
1138
1139    #[test]
1140    fn test_metal_backend_creation() {
1141        let config = MetalConfig::default();
1142        let backend = MetalMemoryBackend::new(config);
1143        assert!(backend.is_ok());
1144    }
1145
1146    #[test]
1147    fn test_memory_pool() {
1148        let mut pool = MetalMemoryPool::new(MetalMemoryType::Private, 1024 * 1024);
1149        let ptr = pool.allocate(1024);
1150        assert!(ptr.is_ok());
1151
1152        let ptr = ptr.expect("unwrap failed");
1153        let result = pool.free(ptr);
1154        assert!(result.is_ok());
1155    }
1156
1157    #[test]
1158    fn test_command_manager() {
1159        let mut manager = MetalCommandManager::new(MetalCommandConfig::default());
1160        let queue_id =
1161            manager.create_command_queue(Some("test".to_string()), MetalQueuePriority::Normal);
1162        assert!(queue_id.is_ok());
1163
1164        let queue_id = queue_id.expect("unwrap failed");
1165        let buffer_id = manager.create_command_buffer(queue_id);
1166        assert!(buffer_id.is_ok());
1167    }
1168
1169    #[test]
1170    fn test_thread_safe_backend() {
1171        let config = MetalConfig::default();
1172        let backend = ThreadSafeMetalBackend::new(config);
1173        assert!(backend.is_ok());
1174
1175        let backend = backend.expect("unwrap failed");
1176        let stats = backend.get_stats();
1177        assert_eq!(stats.total_allocations, 0);
1178    }
1179}