apple-metal-rs
Safe Rust bindings for Apple's Metal
framework on macOS, backed by a Swift bridge in the
screencapturekit-rs style.
apple-metal covers:
- device discovery and capability queries
- buffers (including buffers created from bytes), 1D/2D/3D/array/cube/multisample
textures, texture views, buffer-backed textures, and
IOSurfacezero-copy interop pixel_format::bytes_per_pixel, a byte-size table for everyMTLPixelFormatin the SDK, used by the crate's own CPU transfer and texture-view checks- command queues/buffers with scheduled/completed handlers, plus explicit blit, compute, and render encoders
- MSL compilation, functions, compute/render pipeline state, and descriptor-driven compute/render/tile pipeline creation
- depth/stencil state, sampler state, and descriptor-driven argument encoders
MetalFXspatial / temporal scaler support plus the broaderMetalFXbase / denoised / frame-interpolator symbol families- heaps, events, shared events with listener notifications, dynamic libraries, binary archives, indirect command buffers, acceleration-structure handles, visible / intersection function tables, counter sample buffers, log state, residency sets, and capture scopes
- a named Rust item for every audited top-level
Metal.framework+MetalFX.frameworksymbol; most of those are opaque handles,new()-only classes, or raw integer values without methods
Metal 4 is not wrapped: the MTL4* families and MTLTensor are opaque handles
without constructors or methods. See COVERAGE.md for what the
audit counts and which families are functional. MetalPerformanceShaders
remains out of scope for this crate.
Requirements
- macOS 11 or later and a Swift toolchain (Xcode or the Command Line Tools) to build the bridge.
- Residency sets and shader log state need macOS 15:
new_residency_setandnew_log_statereturn an error on older systems. TheMetalFXscalers need macOS 13, and their constructors returnNonebefore that.
[]
= "0.10"
Only one apple-metal release from 0.10 on can be linked into a binary: the
package declares links = "apple_metal_bridge", so Cargo rejects a graph with
two of them. Its Swift bridge (AppleMetalSwiftBridge, C symbols ametal_*)
shares no symbol with the AppleMetalBridge of apple-metal 0.9 and earlier, so
an older copy pulled in by another crate still links, but its types don't
interoperate with this one.
Quick start
use ;
let device = system_default.expect;
println!;
let _queue = device.new_command_queue.expect;
let buffer = device
.new_buffer
.expect;
println!;
unsafe
Resource safety contracts
MetalBuffer::map_read and map_write return scoped guards and serialize CPU
mappings across clones of the same Rust handle. They are unsafe because the
caller must still exclude overlapping GPU access; managed readback also
requires a completed blit synchronize_resource. Private buffers reject CPU
mapping and can create a shared staging buffer for explicit blit transfers.
Command buffers share lifecycle state across clones. Encoding and submission
methods return Result, commit is rejected while an encoder is active, encoder
drop ends encoding exactly once, and repeated submission or post-completion
encoding is rejected. Encoders also reject waiting on a fence after updating
that same fence. The unretained-reference constructor is unsafe; regular
command buffers remain the default. Resources written into argument buffers are
retained by the native argument buffer, keyed by encoded offset and binding,
until that argument buffer is released, and are automatically declared with
useResource before dispatch or draw, including vertex-stage hazard tracking
for render encoders. Argument setters are available only through an unsafe
scoped destination binding that holds the buffer's CPU mapping lock.
Self-referential and nested retention cycles are rejected.
Descriptor layouts reject empty or unsupported native configurations before
calling Metal and do not impose an artificial binding-index ceiling.
Unsafe texture upload and readback return TextureTransferError after checked
format-layout, stride, byte-length, mip, slice, region, storage-mode, and native
integer validation. Pixel sizes come from pixel_format::bytes_per_pixel, which
is checked against the SDK's MTLPixelFormat.h (BGRA10_XR is 8 bytes). The caller must exclude overlapping GPU and CPU/native
aliases, including buffer mappings behind buffer-backed textures. Private
textures require a GPU staging path rather than CPU transfer methods.
MetalBuffer::new_texture_view_2d returns TextureViewError unless the view
fits inside the buffer and its offset and row stride meet the device's linear
texture alignment. MetalDevice::new_texture returns None for descriptors
Metal would abort on (unknown formats or usage bits, extents beyond 16384, or
2048 for 3D textures and array layers, unsupported sample counts or formats).
Render passes take an optional depth and stencil attachment, and encoders
refuse what Metal would reject or crash on: drawing or dispatching without a
pipeline, a pipeline whose attachment formats or sample count differ from the
pass, a depth- or stencil-testing DepthStencilState without that attachment,
threadgroups above the pipeline maximum (or not a multiple of the execution
width when the pipeline requires it), and samplers without
support_argument_buffers in argument buffers. Buffer resource options,
state descriptors and MetalFX scaler descriptors are checked before Metal sees
them. MetalDevice::new_tensor creates MTLTensors (macOS 26+) from a checked
TensorDescriptor.
Encoding from other frameworks
CommandBuffer::encode_foreign(|foreign| ..) lets code outside this crate (MPS,
SceneKit, SpriteKit, VideoToolbox, your own Objective-C) encode into a command
buffer that apple-metal manages. It returns ActiveEncoder if an apple-metal
encoder is open and InvalidState if the buffer is no longer recording
(checked against both the tracked and the native status), and runs the closure
otherwise. While the closure runs, the buffer's encoder slot is taken: commit,
enqueue, event encoding, new apple-metal encoders and nested encode_foreign
calls on any clone, from any thread, fail with ActiveEncoder instead of
racing or deadlocking. The closure's result is returned in Ok.
ForeignEncoding::command_buffer() is a borrowed (+0) id<MTLCommandBuffer>.
Using it needs unsafe, and the caller's unsafe code must:
- only encode work into it, ending every encoder it creates before the closure returns;
- not commit, enqueue, wait on or release it, or keep using it after the closure returns;
- keep every resource the encoded commands use alive as the command buffer's retention mode requires (unretained-reference buffers retain nothing).
If the closure panics, the slot stays taken, so apple-metal refuses to encode into or commit that buffer again; drop it. If foreign code commits the buffer anyway, apple-metal notices from the native status and refuses to commit it a second time.
Command-buffer handlers, shared-event notifications and the device observer
take Send + 'static Rust closures. Metal owns them until it releases its
block, so they may run after the Rust wrapper is dropped; panics are contained.
A handler whose command buffer is released without being committed receives
CommandBufferError::NotExecuted. Dropping a MetalDeviceObserver removes the
observer before its closure is freed.
Zero-copy from IOSurface
With the default iosurface feature:
#
#
#
create_metal_texture uses the selected plane's actual width, height, and row
stride, including odd bi-planar dimensions, and returns IOSurfaceMetalError
for unsupported formats or incompatible layouts. Packed l10r surfaces are
reported as unsupported rather than mapped to a Metal format with an uncertain
storage representation.
Examples
01_get_device— create the default Metal device and print basic identity.02_caps_buffer_texture— inspect device capabilities, allocate buffers, and create textures.03_command_buffer_blit— submit a simple blit copy on the GPU.04_compute_shader— compile MSL source and dispatch a compute kernel.05_render_and_explicit_encoders— exercise explicit blit, compute, and render encoders in one program.06_resources_and_archives— use argument encoders, heaps, log state, dynamic libraries, and binary archives.07_advanced_objects— touch shared events, fences, counters, indirect command buffers, residency sets, and capture scopes.
Run one directly with:
Status
- The symbol audit was generated against
MacOSX26.2.sdk/System/Library/Frameworks/Metal.framework/Headers; the pixel-format table was checked againstMacOSX26.5.sdk. COVERAGE.mdtracks implemented, partial, and deferred Metal families.- The crate continues to prefer safe, synchronous handle wrappers over raw Objective-C messaging from Rust.