//! Metal GPU framework FFI bindings for macOS. //! //! Provides minimal wrappers around Metal device, command queue, and //! CAMetalLayer for GPU-accelerated rendering. //! //! # Safety //! //! This module contains `unsafe` code for FFI with Metal.framework and //! QuartzCore.framework. The `platform` crate is one of the few crates //! where `unsafe` is permitted. use crate::cf::CfString; use crate::objc::Id; use crate::{class, msg_send}; use std::collections::HashMap; use std::os::raw::c_void; // --------------------------------------------------------------------------- // Framework links // --------------------------------------------------------------------------- #[link(name = "Metal", kind = "framework")] extern "C" { fn MTLCreateSystemDefaultDevice() -> *mut c_void; } #[link(name = "QuartzCore", kind = "framework")] extern "C" {} // --------------------------------------------------------------------------- // Metal pixel format constants // --------------------------------------------------------------------------- /// `MTLPixelFormatR8Unorm` — single-channel 8-bit, unsigned normalized. pub const MTL_PIXEL_FORMAT_R8_UNORM: u64 = 10; /// `MTLPixelFormatRGBA8Unorm` — 8-bit RGBA, unsigned normalized. pub const MTL_PIXEL_FORMAT_RGBA8_UNORM: u64 = 70; /// `MTLPixelFormatBGRA8Unorm` — 8-bit BGRA, unsigned normalized. pub const MTL_PIXEL_FORMAT_BGRA8_UNORM: u64 = 80; // --------------------------------------------------------------------------- // MTLLoadAction / MTLStoreAction constants // --------------------------------------------------------------------------- /// `MTLLoadActionClear` — clear the attachment at the start of a render pass. const MTL_LOAD_ACTION_CLEAR: u64 = 2; /// `MTLStoreActionStore` — store the rendered contents. const MTL_STORE_ACTION_STORE: u64 = 1; // --------------------------------------------------------------------------- // MTLClearColor // --------------------------------------------------------------------------- /// `MTLClearColor` — RGBA clear color for render pass attachments. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct ClearColor { pub red: f64, pub green: f64, pub blue: f64, pub alpha: f64, } impl ClearColor { pub fn new(red: f64, green: f64, blue: f64, alpha: f64) -> ClearColor { ClearColor { red, green, blue, alpha, } } } // --------------------------------------------------------------------------- // CGSize (needed for CAMetalLayer drawable size) // --------------------------------------------------------------------------- /// `CGSize` for setting `CAMetalLayer.drawableSize`. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct MetalSize { pub width: f64, pub height: f64, } // --------------------------------------------------------------------------- // MTLPrimitiveType constants // --------------------------------------------------------------------------- /// `MTLPrimitiveTypeTriangle` — render triangles from triples of vertices. const MTL_PRIMITIVE_TYPE_TRIANGLE: u64 = 3; // --------------------------------------------------------------------------- // MTLBlendFactor constants // --------------------------------------------------------------------------- /// `MTLBlendFactorSourceAlpha` const MTL_BLEND_FACTOR_SOURCE_ALPHA: u64 = 4; /// `MTLBlendFactorOneMinusSourceAlpha` const MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA: u64 = 5; // --------------------------------------------------------------------------- // MTLRegion (for texture upload) // --------------------------------------------------------------------------- #[repr(C)] #[derive(Clone, Copy)] struct MtlOrigin { x: u64, y: u64, z: u64, } #[repr(C)] #[derive(Clone, Copy)] struct MtlSize { width: u64, height: u64, depth: u64, } #[repr(C)] #[derive(Clone, Copy)] struct MtlRegion { origin: MtlOrigin, size: MtlSize, } // --------------------------------------------------------------------------- // Vertex format for 2D rendering // --------------------------------------------------------------------------- /// A vertex for 2D quad rendering via the Metal pipeline. /// /// Layout matches the MSL `Vertex` struct (packed floats, 36 bytes total). #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Vertex { /// Position in pixel coordinates. pub position: [f32; 2], /// RGBA color (0.0–1.0). pub color: [f32; 4], /// UV texture coordinates (unused for solid-color quads). pub tex_coord: [f32; 2], /// 0.0 = solid color, 1.0 = sample from texture. pub use_texture: f32, } /// Uniform data passed to the vertex shader. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Uniforms { /// Viewport dimensions in pixels (width, height). pub viewport_size: [f32; 2], } /// A 2D axis-aligned rectangle for batched rendering. #[derive(Debug, Clone, Copy)] pub struct Quad { /// X position in pixels. pub x: f32, /// Y position in pixels. pub y: f32, /// Width in pixels. pub width: f32, /// Height in pixels. pub height: f32, /// RGBA color (0.0–1.0). pub color: [f32; 4], } impl Quad { /// Decompose this quad into 6 vertices (2 triangles) and append to `out`. pub fn triangulate(&self, out: &mut Vec) { let x0 = self.x; let y0 = self.y; let x1 = self.x + self.width; let y1 = self.y + self.height; let c = self.color; let tc = [0.0, 0.0]; let ut = 0.0; // Triangle 1: top-left, top-right, bottom-left out.push(Vertex { position: [x0, y0], color: c, tex_coord: tc, use_texture: ut, }); out.push(Vertex { position: [x1, y0], color: c, tex_coord: tc, use_texture: ut, }); out.push(Vertex { position: [x0, y1], color: c, tex_coord: tc, use_texture: ut, }); // Triangle 2: top-right, bottom-right, bottom-left out.push(Vertex { position: [x1, y0], color: c, tex_coord: tc, use_texture: ut, }); out.push(Vertex { position: [x1, y1], color: c, tex_coord: tc, use_texture: ut, }); out.push(Vertex { position: [x0, y1], color: c, tex_coord: tc, use_texture: ut, }); } } // --------------------------------------------------------------------------- // MSL shader source // --------------------------------------------------------------------------- /// Metal Shading Language source for the 2D rendering pipeline. /// /// Vertex shader: converts pixel coordinates to clip space using a viewport /// uniform. Fragment shader: solid-color fill or textured sampling with /// color tint (selected per-vertex via `use_texture`). const SHADER_SOURCE: &str = r#" #include using namespace metal; struct Vertex { packed_float2 position; packed_float4 color; packed_float2 tex_coord; float use_texture; }; struct Uniforms { float2 viewport_size; }; struct VertexOut { float4 position [[position]]; float4 color; float2 tex_coord; float use_texture; }; vertex VertexOut vertex_main( const device Vertex* vertices [[buffer(0)]], constant Uniforms& uniforms [[buffer(1)]], uint vid [[vertex_id]] ) { VertexOut out; float2 pos = float2(vertices[vid].position); out.position = float4( (pos.x / uniforms.viewport_size.x) * 2.0 - 1.0, 1.0 - (pos.y / uniforms.viewport_size.y) * 2.0, 0.0, 1.0 ); out.color = float4(vertices[vid].color); out.tex_coord = float2(vertices[vid].tex_coord); out.use_texture = vertices[vid].use_texture; return out; } fragment float4 fragment_main( VertexOut in [[stage_in]], texture2d tex [[texture(0)]], sampler samp [[sampler(0)]] ) { if (in.use_texture > 0.5) { float4 tex_color = tex.sample(samp, in.tex_coord); return tex_color * in.color; } return in.color; } "#; // --------------------------------------------------------------------------- // Buffer — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct Buffer { id: Id, length: usize, } impl Buffer { /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.id } /// Return the buffer size in bytes. pub fn length(&self) -> usize { self.length } } // --------------------------------------------------------------------------- // RenderPipeline — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct RenderPipeline { id: Id, } // --------------------------------------------------------------------------- // Texture — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct Texture { id: Id, width: u32, height: u32, } impl Texture { /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.id } /// Texture width in pixels. pub fn width(&self) -> u32 { self.width } /// Texture height in pixels. pub fn height(&self) -> u32 { self.height } } // --------------------------------------------------------------------------- // SamplerState — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct SamplerState { id: Id, } // --------------------------------------------------------------------------- // Device — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct Device { id: Id, } impl Device { /// Obtain the system default Metal GPU device. /// /// Returns `None` if no Metal-capable GPU is available. pub fn system_default() -> Option { let ptr = unsafe { MTLCreateSystemDefaultDevice() }; let id = unsafe { Id::from_raw(ptr as *mut _) }?; Some(Device { id }) } /// Create a new command queue on this device. pub fn new_command_queue(&self) -> Option { let queue: *mut c_void = msg_send![self.id.as_ptr(), newCommandQueue]; let id = unsafe { Id::from_raw(queue as *mut _) }?; Some(CommandQueue { id }) } /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.id } /// Compile MSL source into a library. pub fn new_library_with_source(&self, source: &str) -> Option { let ns_source = CfString::new(source)?; let mut error_ptr: *mut c_void = std::ptr::null_mut(); let library: *mut c_void = msg_send![ self.id.as_ptr(), newLibraryWithSource: ns_source.as_void_ptr() as *mut c_void, options: std::ptr::null_mut::(), error: &mut error_ptr as *mut *mut c_void ]; unsafe { Id::from_raw(library as *mut _) } } /// Create a render pipeline state from a descriptor. pub fn new_render_pipeline_state(&self, descriptor: Id) -> Option { let mut error_ptr: *mut c_void = std::ptr::null_mut(); let state: *mut c_void = msg_send![ self.id.as_ptr(), newRenderPipelineStateWithDescriptor: descriptor.as_ptr(), error: &mut error_ptr as *mut *mut c_void ]; let id = unsafe { Id::from_raw(state as *mut _) }?; Some(RenderPipeline { id }) } /// Create a buffer from vertex data. pub fn new_buffer_with_vertices(&self, vertices: &[Vertex]) -> Option { let byte_len = std::mem::size_of_val(vertices); if byte_len == 0 { return None; } let buf: *mut c_void = msg_send![ self.id.as_ptr(), newBufferWithBytes: vertices.as_ptr() as *mut c_void, length: byte_len as u64, options: 0u64 ]; let id = unsafe { Id::from_raw(buf as *mut _) }?; Some(Buffer { id, length: byte_len, }) } /// Create a 1×1 white BGRA texture (used as a dummy when no texture is bound). pub fn new_dummy_texture(&self) -> Option { // Create texture descriptor via class method let cls = class!("MTLTextureDescriptor")?; let desc: *mut c_void = msg_send![ cls.as_ptr(), texture2DDescriptorWithPixelFormat: MTL_PIXEL_FORMAT_BGRA8_UNORM, width: 1u64, height: 1u64, mipmapped: false ]; let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; // Set usage to ShaderRead (1) let _: *mut c_void = msg_send![desc_id.as_ptr(), setUsage: 1u64]; // Create texture let tex: *mut c_void = msg_send![self.id.as_ptr(), newTextureWithDescriptor: desc_id.as_ptr()]; let tex_id = unsafe { Id::from_raw(tex as *mut _) }?; // Upload 1 white pixel (BGRA: 0xFF 0xFF 0xFF 0xFF) let pixel: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF]; let region = MtlRegion { origin: MtlOrigin { x: 0, y: 0, z: 0 }, size: MtlSize { width: 1, height: 1, depth: 1, }, }; let _: *mut c_void = msg_send![ tex_id.as_ptr(), replaceRegion: region, mipmapLevel: 0u64, withBytes: pixel.as_ptr() as *mut c_void, bytesPerRow: 4u64 ]; Some(Texture { id: tex_id, width: 1, height: 1, }) } /// Create a texture with the given pixel format and upload pixel data. /// /// `bytes_per_row` is the stride in bytes between consecutive rows. pub fn new_texture( &self, width: u32, height: u32, pixel_format: u64, data: &[u8], bytes_per_row: u32, ) -> Option { if width == 0 || height == 0 || data.is_empty() { return None; } let required = bytes_per_row as usize * height as usize; if data.len() < required { return None; } let cls = class!("MTLTextureDescriptor")?; let desc: *mut c_void = msg_send![ cls.as_ptr(), texture2DDescriptorWithPixelFormat: pixel_format, width: width as u64, height: height as u64, mipmapped: false ]; let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; // Set usage to ShaderRead (1) let _: *mut c_void = msg_send![desc_id.as_ptr(), setUsage: 1u64]; let tex: *mut c_void = msg_send![self.id.as_ptr(), newTextureWithDescriptor: desc_id.as_ptr()]; let tex_id = unsafe { Id::from_raw(tex as *mut _) }?; let region = MtlRegion { origin: MtlOrigin { x: 0, y: 0, z: 0 }, size: MtlSize { width: width as u64, height: height as u64, depth: 1, }, }; let _: *mut c_void = msg_send![ tex_id.as_ptr(), replaceRegion: region, mipmapLevel: 0u64, withBytes: data.as_ptr() as *mut c_void, bytesPerRow: bytes_per_row as u64 ]; Some(Texture { id: tex_id, width, height, }) } /// Create a texture from RGBA8 pixel data. pub fn new_texture_rgba8(&self, width: u32, height: u32, data: &[u8]) -> Option { self.new_texture(width, height, MTL_PIXEL_FORMAT_RGBA8_UNORM, data, width * 4) } /// Create a texture from single-channel grayscale (R8) pixel data. pub fn new_texture_r8(&self, width: u32, height: u32, data: &[u8]) -> Option { self.new_texture(width, height, MTL_PIXEL_FORMAT_R8_UNORM, data, width) } /// Create a sampler state with linear filtering. pub fn new_sampler_state(&self) -> Option { let cls = class!("MTLSamplerDescriptor")?; let desc: *mut c_void = msg_send![cls.as_ptr(), alloc]; let desc: *mut c_void = msg_send![desc, init]; let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; // MTLSamplerMinMagFilterLinear = 1 let _: *mut c_void = msg_send![desc_id.as_ptr(), setMinFilter: 1u64]; let _: *mut c_void = msg_send![desc_id.as_ptr(), setMagFilter: 1u64]; let sampler: *mut c_void = msg_send![self.id.as_ptr(), newSamplerStateWithDescriptor: desc_id.as_ptr()]; let id = unsafe { Id::from_raw(sampler as *mut _) }?; Some(SamplerState { id }) } /// Create a sampler state with nearest-neighbor filtering and clamp-to-edge. pub fn new_sampler_state_nearest(&self) -> Option { let cls = class!("MTLSamplerDescriptor")?; let desc: *mut c_void = msg_send![cls.as_ptr(), alloc]; let desc: *mut c_void = msg_send![desc, init]; let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; // MTLSamplerMinMagFilterNearest = 0 let _: *mut c_void = msg_send![desc_id.as_ptr(), setMinFilter: 0u64]; let _: *mut c_void = msg_send![desc_id.as_ptr(), setMagFilter: 0u64]; // MTLSamplerAddressModeClampToEdge = 0 (default, but explicit for clarity) let _: *mut c_void = msg_send![desc_id.as_ptr(), setSAddressMode: 0u64]; let _: *mut c_void = msg_send![desc_id.as_ptr(), setTAddressMode: 0u64]; let sampler: *mut c_void = msg_send![self.id.as_ptr(), newSamplerStateWithDescriptor: desc_id.as_ptr()]; let id = unsafe { Id::from_raw(sampler as *mut _) }?; Some(SamplerState { id }) } } // --------------------------------------------------------------------------- // CommandQueue — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct CommandQueue { id: Id, } impl CommandQueue { /// Create a new command buffer from this queue. pub fn command_buffer(&self) -> Option { let buf: *mut c_void = msg_send![self.id.as_ptr(), commandBuffer]; let id = unsafe { Id::from_raw(buf as *mut _) }?; Some(CommandBuffer { id }) } } // --------------------------------------------------------------------------- // CommandBuffer — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct CommandBuffer { id: Id, } impl CommandBuffer { /// Create a render command encoder with the given descriptor. pub fn render_command_encoder(&self, descriptor: Id) -> Option { let encoder: *mut c_void = msg_send![self.id.as_ptr(), renderCommandEncoderWithDescriptor: descriptor.as_ptr()]; let id = unsafe { Id::from_raw(encoder as *mut _) }?; Some(RenderCommandEncoder { id }) } /// Schedule presentation of a drawable. pub fn present_drawable(&self, drawable: Id) { let _: *mut c_void = msg_send![self.id.as_ptr(), presentDrawable: drawable.as_ptr()]; } /// Commit the command buffer for execution. pub fn commit(&self) { let _: *mut c_void = msg_send![self.id.as_ptr(), commit]; } } // --------------------------------------------------------------------------- // RenderCommandEncoder — wraps id // --------------------------------------------------------------------------- /// Wrapper around `id`. pub struct RenderCommandEncoder { id: Id, } impl RenderCommandEncoder { /// Set the render pipeline state for subsequent draw calls. pub fn set_render_pipeline_state(&self, pipeline: &RenderPipeline) { let _: *mut c_void = msg_send![self.id.as_ptr(), setRenderPipelineState: pipeline.id.as_ptr()]; } /// Bind a vertex buffer at the given index. pub fn set_vertex_buffer(&self, buffer: &Buffer, offset: u64, index: u64) { let _: *mut c_void = msg_send![ self.id.as_ptr(), setVertexBuffer: buffer.id.as_ptr(), offset: offset, atIndex: index ]; } /// Pass uniform data directly to the vertex shader at the given index. pub fn set_vertex_bytes(&self, data: &T, index: u64) { let length = std::mem::size_of::() as u64; let _: *mut c_void = msg_send![ self.id.as_ptr(), setVertexBytes: data as *const T as *mut c_void, length: length, atIndex: index ]; } /// Bind a texture to the fragment shader at the given index. pub fn set_fragment_texture(&self, texture: &Texture, index: u64) { let _: *mut c_void = msg_send![ self.id.as_ptr(), setFragmentTexture: texture.id.as_ptr(), atIndex: index ]; } /// Bind a sampler state to the fragment shader at the given index. pub fn set_fragment_sampler_state(&self, sampler: &SamplerState, index: u64) { let _: *mut c_void = msg_send![ self.id.as_ptr(), setFragmentSamplerState: sampler.id.as_ptr(), atIndex: index ]; } /// Issue a draw call for non-indexed primitives. pub fn draw_primitives(&self, primitive_type: u64, vertex_start: u64, vertex_count: u64) { let _: *mut c_void = msg_send![ self.id.as_ptr(), drawPrimitives: primitive_type, vertexStart: vertex_start, vertexCount: vertex_count ]; } /// End encoding commands. pub fn end_encoding(&self) { let _: *mut c_void = msg_send![self.id.as_ptr(), endEncoding]; } } // --------------------------------------------------------------------------- // MetalLayer — wraps CAMetalLayer // --------------------------------------------------------------------------- /// Wrapper around `CAMetalLayer`. pub struct MetalLayer { id: Id, } impl MetalLayer { /// Create a new `CAMetalLayer`. pub fn new() -> Option { let cls = class!("CAMetalLayer")?; let layer: *mut c_void = msg_send![cls.as_ptr(), alloc]; let layer: *mut c_void = msg_send![layer, init]; let id = unsafe { Id::from_raw(layer as *mut _) }?; Some(MetalLayer { id }) } /// Set the Metal device for this layer. pub fn set_device(&self, device: &Device) { let _: *mut c_void = msg_send![self.id.as_ptr(), setDevice: device.id().as_ptr()]; } /// Set the pixel format. pub fn set_pixel_format(&self, format: u64) { let _: *mut c_void = msg_send![self.id.as_ptr(), setPixelFormat: format]; } /// Set whether the layer's textures are for framebuffer use only. pub fn set_framebuffer_only(&self, val: bool) { let _: *mut c_void = msg_send![self.id.as_ptr(), setFramebufferOnly: val]; } /// Set the drawable size in pixels. pub fn set_drawable_size(&self, width: f64, height: f64) { let size = MetalSize { width, height }; let _: *mut c_void = msg_send![self.id.as_ptr(), setDrawableSize: size]; } /// Get the next drawable from the layer. /// /// Returns the `CAMetalDrawable` object, or `None` if no drawable is available. pub fn next_drawable(&self) -> Option { let drawable: *mut c_void = msg_send![self.id.as_ptr(), nextDrawable]; unsafe { Id::from_raw(drawable as *mut _) } } /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.id } } // --------------------------------------------------------------------------- // Render pass descriptor helpers // --------------------------------------------------------------------------- /// Create a `MTLRenderPassDescriptor` configured to clear to the given color. /// /// Sets up color attachment 0 with: /// - `texture` from the drawable /// - `loadAction = MTLLoadActionClear` /// - `storeAction = MTLStoreActionStore` /// - `clearColor` as specified pub fn make_clear_pass_descriptor(drawable_texture: Id, clear_color: ClearColor) -> Option { let cls = class!("MTLRenderPassDescriptor")?; let desc: *mut c_void = msg_send![cls.as_ptr(), renderPassDescriptor]; let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; // Get colorAttachments[0] let attachments: *mut c_void = msg_send![desc_id.as_ptr(), colorAttachments]; let attachment: *mut c_void = msg_send![attachments, objectAtIndexedSubscript: 0u64]; // Set texture let _: *mut c_void = msg_send![attachment, setTexture: drawable_texture.as_ptr()]; // Set load action = Clear let _: *mut c_void = msg_send![attachment, setLoadAction: MTL_LOAD_ACTION_CLEAR]; // Set store action = Store let _: *mut c_void = msg_send![attachment, setStoreAction: MTL_STORE_ACTION_STORE]; // Set clear color let _: *mut c_void = msg_send![attachment, setClearColor: clear_color]; Some(desc_id) } /// Get the `texture` property of a `CAMetalDrawable`. pub fn drawable_texture(drawable: Id) -> Option { let texture: *mut c_void = msg_send![drawable.as_ptr(), texture]; unsafe { Id::from_raw(texture as *mut _) } } // --------------------------------------------------------------------------- // Render pipeline creation // --------------------------------------------------------------------------- /// Create the 2D render pipeline state from compiled MSL shaders. /// /// Configures alpha blending: `source * sourceAlpha + dest * (1 - sourceAlpha)`. fn create_render_pipeline(device: &Device) -> Option { // Compile shader source let library = device.new_library_with_source(SHADER_SOURCE)?; // Get vertex and fragment functions let vert_name = CfString::new("vertex_main")?; let frag_name = CfString::new("fragment_main")?; let vert_fn: *mut c_void = msg_send![library.as_ptr(), newFunctionWithName: vert_name.as_void_ptr() as *mut c_void]; let vert_fn_id = unsafe { Id::from_raw(vert_fn as *mut _) }?; let frag_fn: *mut c_void = msg_send![library.as_ptr(), newFunctionWithName: frag_name.as_void_ptr() as *mut c_void]; let frag_fn_id = unsafe { Id::from_raw(frag_fn as *mut _) }?; // Create pipeline descriptor let cls = class!("MTLRenderPipelineDescriptor")?; let desc: *mut c_void = msg_send![cls.as_ptr(), alloc]; let desc: *mut c_void = msg_send![desc, init]; let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; // Set vertex and fragment functions let _: *mut c_void = msg_send![desc_id.as_ptr(), setVertexFunction: vert_fn_id.as_ptr()]; let _: *mut c_void = msg_send![desc_id.as_ptr(), setFragmentFunction: frag_fn_id.as_ptr()]; // Configure color attachment 0 let attachments: *mut c_void = msg_send![desc_id.as_ptr(), colorAttachments]; let attachment: *mut c_void = msg_send![attachments, objectAtIndexedSubscript: 0u64]; // Pixel format = BGRA8Unorm let _: *mut c_void = msg_send![attachment, setPixelFormat: MTL_PIXEL_FORMAT_BGRA8_UNORM]; // Enable alpha blending let _: *mut c_void = msg_send![attachment, setBlendingEnabled: true]; // source * sourceAlpha + dest * (1 - sourceAlpha) let _: *mut c_void = msg_send![attachment, setSourceRGBBlendFactor: MTL_BLEND_FACTOR_SOURCE_ALPHA]; let _: *mut c_void = msg_send![ attachment, setDestinationRGBBlendFactor: MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA ]; let _: *mut c_void = msg_send![attachment, setSourceAlphaBlendFactor: MTL_BLEND_FACTOR_SOURCE_ALPHA]; let _: *mut c_void = msg_send![ attachment, setDestinationAlphaBlendFactor: MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA ]; device.new_render_pipeline_state(desc_id) } // --------------------------------------------------------------------------- // MetalRenderer — batched 2D quad renderer // --------------------------------------------------------------------------- /// A batched 2D renderer using the Metal GPU pipeline. /// /// Renders solid-color and textured quads via a single vertex buffer and /// draw call per batch. Alpha blending is enabled for semi-transparent fills. pub struct MetalRenderer { device: Device, pipeline: RenderPipeline, dummy_texture: Texture, sampler: SamplerState, } impl MetalRenderer { /// Create a new `MetalRenderer` on the given device. /// /// Compiles the MSL shaders, creates the pipeline state, and sets up a /// dummy 1×1 white texture for solid-color rendering. /// /// Returns `None` if shader compilation or pipeline creation fails. pub fn new(device: Device) -> Option { let pipeline = create_render_pipeline(&device)?; let dummy_texture = device.new_dummy_texture()?; let sampler = device.new_sampler_state()?; Some(MetalRenderer { device, pipeline, dummy_texture, sampler, }) } /// Build a vertex buffer from a batch of quads. /// /// Each quad is decomposed into 2 triangles (6 vertices). Returns the /// buffer and the total vertex count. pub fn build_vertex_buffer(&self, quads: &[Quad]) -> Option<(Buffer, usize)> { let mut vertices = Vec::with_capacity(quads.len() * 6); for quad in quads { quad.triangulate(&mut vertices); } let count = vertices.len(); let buffer = self.device.new_buffer_with_vertices(&vertices)?; Some((buffer, count)) } /// Encode draw commands for a batch of quads into a render command encoder. /// /// The encoder must already be created from a command buffer with an /// appropriate render pass descriptor. pub fn encode_draw( &self, encoder: &RenderCommandEncoder, vertex_buffer: &Buffer, vertex_count: usize, viewport_width: f32, viewport_height: f32, ) { let uniforms = Uniforms { viewport_size: [viewport_width, viewport_height], }; encoder.set_render_pipeline_state(&self.pipeline); encoder.set_vertex_buffer(vertex_buffer, 0, 0); encoder.set_vertex_bytes(&uniforms, 1); encoder.set_fragment_texture(&self.dummy_texture, 0); encoder.set_fragment_sampler_state(&self.sampler, 0); encoder.draw_primitives(MTL_PRIMITIVE_TYPE_TRIANGLE, 0, vertex_count as u64); } /// Return a reference to the underlying device. pub fn device(&self) -> &Device { &self.device } } // --------------------------------------------------------------------------- // TextureCache — avoid redundant GPU uploads // --------------------------------------------------------------------------- /// Cache of GPU textures keyed by node ID. /// /// Avoids re-uploading unchanged images to the GPU on every frame. /// Call [`remove`] or [`clear`] to release textures when nodes are removed. pub struct TextureCache { entries: HashMap, } impl Default for TextureCache { fn default() -> Self { Self::new() } } impl TextureCache { /// Create an empty texture cache. pub fn new() -> Self { TextureCache { entries: HashMap::new(), } } /// Look up a cached texture by node ID. pub fn get(&self, node_id: usize) -> Option<&Texture> { self.entries.get(&node_id) } /// Insert or replace a texture for the given node ID. pub fn insert(&mut self, node_id: usize, texture: Texture) { self.entries.insert(node_id, texture); } /// Remove a texture from the cache, returning it if present. pub fn remove(&mut self, node_id: usize) -> Option { self.entries.remove(&node_id) } /// Remove all cached textures. pub fn clear(&mut self) { self.entries.clear(); } /// Return the number of cached textures. pub fn len(&self) -> usize { self.entries.len() } /// Return `true` if the cache is empty. pub fn is_empty(&self) -> bool { self.entries.is_empty() } /// Return `true` if the cache contains a texture for the given node ID. pub fn contains(&self, node_id: usize) -> bool { self.entries.contains_key(&node_id) } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn clear_color_new() { let c = ClearColor::new(0.1, 0.2, 0.3, 1.0); assert_eq!(c.red, 0.1); assert_eq!(c.green, 0.2); assert_eq!(c.blue, 0.3); assert_eq!(c.alpha, 1.0); } #[test] fn metal_size_layout() { let s = MetalSize { width: 800.0, height: 600.0, }; assert_eq!(s.width, 800.0); assert_eq!(s.height, 600.0); } #[test] fn pixel_format_constant() { assert_eq!(MTL_PIXEL_FORMAT_BGRA8_UNORM, 80); } #[test] fn system_default_device() { // On a Mac with Metal support, this should succeed. // On CI without GPU, it may return None — that's OK. let device = Device::system_default(); if let Some(device) = device { assert!(!device.id().as_ptr().is_null()); } } #[test] fn create_command_queue() { let device = match Device::system_default() { Some(d) => d, None => return, // No GPU available }; let queue = device.new_command_queue(); assert!(queue.is_some()); } #[test] fn metal_layer_create() { let _pool = crate::appkit::AutoreleasePool::new(); let layer = MetalLayer::new(); assert!(layer.is_some()); } #[test] fn metal_layer_configure() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let layer = MetalLayer::new().expect("CAMetalLayer should be available"); layer.set_device(&device); layer.set_pixel_format(MTL_PIXEL_FORMAT_BGRA8_UNORM); layer.set_framebuffer_only(true); layer.set_drawable_size(800.0, 600.0); } // -- Vertex layout tests ------------------------------------------------ #[test] fn vertex_size_and_alignment() { // The Vertex struct must be 36 bytes to match the MSL packed layout. assert_eq!(std::mem::size_of::(), 36); assert_eq!(std::mem::align_of::(), 4); } #[test] fn uniforms_size() { assert_eq!(std::mem::size_of::(), 8); } // -- Quad triangulation tests ------------------------------------------- #[test] fn quad_triangulate_produces_6_vertices() { let quad = Quad { x: 10.0, y: 20.0, width: 100.0, height: 50.0, color: [1.0, 0.0, 0.0, 1.0], }; let mut verts = Vec::new(); quad.triangulate(&mut verts); assert_eq!(verts.len(), 6); } #[test] fn quad_triangulate_covers_corners() { let quad = Quad { x: 0.0, y: 0.0, width: 100.0, height: 50.0, color: [1.0, 1.0, 1.0, 1.0], }; let mut verts = Vec::new(); quad.triangulate(&mut verts); // Collect all unique positions let positions: Vec<[f32; 2]> = verts.iter().map(|v| v.position).collect(); // Should have all 4 corners represented assert!(positions.contains(&[0.0, 0.0])); assert!(positions.contains(&[100.0, 0.0])); assert!(positions.contains(&[0.0, 50.0])); assert!(positions.contains(&[100.0, 50.0])); } #[test] fn quad_triangulate_solid_color() { let quad = Quad { x: 0.0, y: 0.0, width: 10.0, height: 10.0, color: [0.5, 0.3, 0.1, 0.8], }; let mut verts = Vec::new(); quad.triangulate(&mut verts); for v in &verts { assert_eq!(v.color, [0.5, 0.3, 0.1, 0.8]); assert_eq!(v.use_texture, 0.0); assert_eq!(v.tex_coord, [0.0, 0.0]); } } #[test] fn multiple_quads_batch() { let quads = vec![ Quad { x: 0.0, y: 0.0, width: 50.0, height: 50.0, color: [1.0, 0.0, 0.0, 1.0], }, Quad { x: 60.0, y: 0.0, width: 50.0, height: 50.0, color: [0.0, 1.0, 0.0, 1.0], }, Quad { x: 120.0, y: 0.0, width: 50.0, height: 50.0, color: [0.0, 0.0, 1.0, 1.0], }, ]; let mut verts = Vec::new(); for q in &quads { q.triangulate(&mut verts); } assert_eq!(verts.len(), 18); // 3 quads × 6 vertices // First quad vertices should be red for v in &verts[..6] { assert_eq!(v.color, [1.0, 0.0, 0.0, 1.0]); } // Second quad vertices should be green for v in &verts[6..12] { assert_eq!(v.color, [0.0, 1.0, 0.0, 1.0]); } } // -- GPU tests (require Metal device) ----------------------------------- #[test] fn compile_shader_source() { let device = match Device::system_default() { Some(d) => d, None => return, }; let library = device.new_library_with_source(SHADER_SOURCE); assert!( library.is_some(), "MSL shader should compile without errors" ); } #[test] fn create_pipeline_state() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let pipeline = create_render_pipeline(&device); assert!( pipeline.is_some(), "render pipeline state should be created" ); } #[test] fn create_vertex_buffer() { let device = match Device::system_default() { Some(d) => d, None => return, }; let quad = Quad { x: 0.0, y: 0.0, width: 100.0, height: 100.0, color: [1.0, 0.0, 0.0, 1.0], }; let mut verts = Vec::new(); quad.triangulate(&mut verts); let buffer = device.new_buffer_with_vertices(&verts); assert!(buffer.is_some()); let buffer = buffer.unwrap(); assert_eq!(buffer.length(), 6 * std::mem::size_of::()); } #[test] fn create_dummy_texture() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let texture = device.new_dummy_texture(); assert!(texture.is_some(), "dummy texture should be created"); } #[test] fn create_sampler_state() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let sampler = device.new_sampler_state(); assert!(sampler.is_some(), "sampler state should be created"); } #[test] fn metal_renderer_new() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let renderer = MetalRenderer::new(device); assert!(renderer.is_some(), "MetalRenderer should be created"); } #[test] fn metal_renderer_build_vertex_buffer() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let renderer = MetalRenderer::new(device).expect("renderer should be created"); let quads = vec![ Quad { x: 10.0, y: 10.0, width: 200.0, height: 100.0, color: [1.0, 0.0, 0.0, 1.0], }, Quad { x: 50.0, y: 50.0, width: 100.0, height: 100.0, color: [0.0, 0.0, 1.0, 0.5], }, ]; let result = renderer.build_vertex_buffer(&quads); assert!(result.is_some()); let (buffer, count) = result.unwrap(); assert_eq!(count, 12); // 2 quads × 6 vertices assert_eq!(buffer.length(), 12 * std::mem::size_of::()); } #[test] fn empty_quad_list() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let renderer = MetalRenderer::new(device).expect("renderer should be created"); let result = renderer.build_vertex_buffer(&[]); assert!(result.is_none(), "empty quad list should return None"); } #[test] fn primitive_type_constant() { assert_eq!(MTL_PRIMITIVE_TYPE_TRIANGLE, 3); } #[test] fn blend_factor_constants() { assert_eq!(MTL_BLEND_FACTOR_SOURCE_ALPHA, 4); assert_eq!(MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA, 5); } // -- Pixel format constants --------------------------------------------- #[test] fn pixel_format_r8_unorm() { assert_eq!(MTL_PIXEL_FORMAT_R8_UNORM, 10); } #[test] fn pixel_format_rgba8_unorm() { assert_eq!(MTL_PIXEL_FORMAT_RGBA8_UNORM, 70); } // -- Texture creation tests --------------------------------------------- #[test] fn create_texture_rgba8() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; // 2×2 red RGBA image let data: Vec = vec![ 255, 0, 0, 255, 0, 255, 0, 255, // row 0 0, 0, 255, 255, 255, 255, 255, 255, // row 1 ]; let texture = device.new_texture_rgba8(2, 2, &data); assert!(texture.is_some(), "RGBA8 texture should be created"); let texture = texture.unwrap(); assert_eq!(texture.width(), 2); assert_eq!(texture.height(), 2); } #[test] fn create_texture_r8() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; // 4×4 grayscale glyph bitmap let data: Vec = vec![ 0, 64, 128, 255, 32, 96, 160, 224, 0, 0, 0, 0, 255, 255, 255, 255, ]; let texture = device.new_texture_r8(4, 4, &data); assert!(texture.is_some(), "R8 texture should be created"); let texture = texture.unwrap(); assert_eq!(texture.width(), 4); assert_eq!(texture.height(), 4); } #[test] fn create_texture_zero_dimension_returns_none() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; assert!(device.new_texture_rgba8(0, 10, &[0; 40]).is_none()); assert!(device.new_texture_rgba8(10, 0, &[0; 40]).is_none()); assert!(device.new_texture_rgba8(10, 10, &[]).is_none()); } #[test] fn create_texture_large() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; // 256×256 RGBA image let data = vec![128u8; 256 * 256 * 4]; let texture = device.new_texture_rgba8(256, 256, &data); assert!(texture.is_some()); let texture = texture.unwrap(); assert_eq!(texture.width(), 256); assert_eq!(texture.height(), 256); } #[test] fn dummy_texture_dimensions() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let texture = device.new_dummy_texture().unwrap(); assert_eq!(texture.width(), 1); assert_eq!(texture.height(), 1); } // -- Sampler tests ------------------------------------------------------ #[test] fn create_sampler_state_nearest() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let sampler = device.new_sampler_state_nearest(); assert!( sampler.is_some(), "nearest-neighbor sampler should be created" ); } // -- TextureCache tests ------------------------------------------------- #[test] fn texture_cache_new_is_empty() { let cache = TextureCache::new(); assert!(cache.is_empty()); assert_eq!(cache.len(), 0); } #[test] fn texture_cache_insert_and_get() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let mut cache = TextureCache::new(); let data = vec![255u8; 4 * 4]; // 2×2 RGBA let texture = device.new_texture_rgba8(2, 2, &data[..16]).unwrap(); cache.insert(42, texture); assert_eq!(cache.len(), 1); assert!(cache.contains(42)); assert!(!cache.contains(99)); let cached = cache.get(42); assert!(cached.is_some()); assert_eq!(cached.unwrap().width(), 2); } #[test] fn texture_cache_remove() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let mut cache = TextureCache::new(); let data = vec![255u8; 16]; let texture = device.new_texture_rgba8(2, 2, &data).unwrap(); cache.insert(1, texture); assert_eq!(cache.len(), 1); let removed = cache.remove(1); assert!(removed.is_some()); assert!(cache.is_empty()); assert!(cache.remove(1).is_none()); } #[test] fn texture_cache_clear() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let mut cache = TextureCache::new(); let data = vec![255u8; 16]; for i in 0..5 { let texture = device.new_texture_rgba8(2, 2, &data).unwrap(); cache.insert(i, texture); } assert_eq!(cache.len(), 5); cache.clear(); assert!(cache.is_empty()); } #[test] fn texture_cache_replace() { let _pool = crate::appkit::AutoreleasePool::new(); let device = match Device::system_default() { Some(d) => d, None => return, }; let mut cache = TextureCache::new(); let data_small = vec![255u8; 4]; // 1×1 let tex1 = device.new_texture_rgba8(1, 1, &data_small).unwrap(); cache.insert(7, tex1); assert_eq!(cache.get(7).unwrap().width(), 1); let data_big = vec![255u8; 16]; // 2×2 let tex2 = device.new_texture_rgba8(2, 2, &data_big).unwrap(); cache.insert(7, tex2); assert_eq!(cache.len(), 1); assert_eq!(cache.get(7).unwrap().width(), 2); } }