Help us improve
Share bugs, ideas, or general feedback.
From game-porting-skills
Guides correct ownership, retain/release, and autorelease pool placement for metal-cpp objects to prevent crashes, leaks, and use-after-free.
npx claudepluginhub apple/game-porting-toolkit --plugin game-porting-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/game-porting-skills:managing-metal-cpp-lifetimesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
metal-cpp types like `MTL::Buffer*` wrap ObjC Metal objects, so the underlying ObjC retain/release rules still apply. Getting ownership wrong causes leaks, use-after-free, or double-free.
Creates and manages Metal 4 GPU resources: buffers, textures, heaps, residency sets, purgeable state, texture view pools, and deferred destruction.
Provides Objective-C ARC patterns for strong/weak references, retain cycles, ownership qualifiers, Core Foundation bridging, and memory-safe code without manual retain/release.
Guides safe C++ memory management using unique_ptr, shared_ptr, weak_ptr, and RAII patterns for exception-safe resource handling.
Share bugs, ideas, or general feedback.
metal-cpp types like MTL::Buffer* wrap ObjC Metal objects, so the underlying ObjC retain/release rules still apply. Getting ownership wrong causes leaks, use-after-free, or double-free.
Methods starting with new, alloc, copy, mutableCopy, or Create return owned (+1) objects. Everything else returns autoreleased objects.
Returned from new/alloc/copy/mutableCopy/Create?
YES → NS::TransferPtr(ptr) // you already own it
NO → need AutoreleasePool in scope
Want to keep beyond pool scope?
YES → NS::RetainPtr(ptr) // retains; survives pool drain
NO → use raw pointer // pool drains it
| API Pattern | Returns | Wrap With |
|---|---|---|
device->new*() | Owned | NS::TransferPtr() |
alloc()->init() | Owned | NS::TransferPtr() |
*CommandEncoder() | Autoreleased | Raw pointer within pool scope |
::descriptor() / ::string() | Autoreleased | NS::RetainPtr() if keeping |
MTL::CreateSystemDefaultDevice() | Owned | NS::TransferPtr() |
NS::TransferPtr(ptr) — Takes ownership without additional retain. For objects you already own.NS::RetainPtr(ptr) — Takes ownership with additional retain. For autoreleased objects you want to keep beyond pool scope.Pools are required in these contexts — without them, autoreleased objects accumulate indefinitely:
drain() releases contents)// Frame pool — create once, drain per frame
auto framePool = NS::TransferPtr(NS::AutoreleasePool::alloc()->init());
// ... each frame:
framePool->drain(); // releases autoreleased objects, pool stays alive for reuse
drain() releases contents but keeps the pool alive. reset() on the SharedPtr destroys the pool.
When porting a project that has ARC translation units alongside metal-cpp, use bridge casts to hand ownership across the boundary:
| Cast | Effect | Use When |
|---|---|---|
(__bridge T) | No retain/release | Temporary access, no ownership change |
(__bridge_retained T) | +1 retain | Handing an owned object to metal-cpp — wrap result in NS::TransferPtr |
(__bridge_transfer T) | C++ releases its +1 | Handing back to ARC from a metal-cpp +1 object |
A __bridge_retained cast produces a +1 object suitable for NS::TransferPtr. Every __bridge_retained must be balanced by a __bridge_transfer or explicit release().
NS::SharedPtr copy across threads is safe (atomic retain)MTL4::CommandBuffer does not retain resources. MTL::CommandBuffer retains by default but can be configured via its descriptor to not retain. When resources are not retained by the command buffer, you must keep them alive (via SharedPtr, deferred destruction queue, or residency set) until the GPU is done with them — a fence or event signals completion. The deferred destruction queue pattern is canonicalised in managing-metal4-resources (frame-delayed release with mutex-protected residency removal).
release() on an autoreleased object — Autoreleased objects (encoders, descriptors, etc.) are released when the pool drains. An explicit release() causes a double-free.TransferPtr on autoreleased object — TransferPtr calls release() on destruction → same double-free as above. Use RetainPtr if you need to keep it past pool scope, otherwise raw pointer within scope.OBJC_DEBUG_MISSING_POOLS=YES — runtime warning when no pool exists on current threadleaks --autoreleasePools — inspect pool contents in a memgraphMTL_HUD_ENABLED=1) — continuously growing memory counter = resource leakRead the relevant metal-cpp header before writing lifetime code — the headers are the source of truth for the ownership helpers and their exact semantics.
Foundation/NSSharedPtr.hpp (NS::SharedPtr, NS::TransferPtr, NS::RetainPtr), Foundation/NSAutoreleasePool.hpp (NS::AutoreleasePool)