On an Apple silicon Mac, draw into a shared MTLBuffer through a bitmap CGContext, then create a Metal texture backed by that same buffer. The essential contracts are matching RGBA bytes, aligned row strides, retained storage, and ordered CPU/GPU access.
This guide builds a synthetic red rectangle and blue oval, then checks that Metal can read the drawing through the texture. It is intended for a Mac developer preparing generated graphics for a desktop app’s Metal renderer. The sharing mechanism is documented by Apple’s makeTexture(descriptor:offset:bytesPerRow:) API.
Which sharing path should you choose?
Allocate the storage with Metal first. Obtain its CPU address with buffer.contents(), use that address as the bitmap context’s destination, and create the texture from the buffer. Apple documents that bitmap drawing updates the supplied memory and that a buffer-backed texture shares its buffer’s allocation. Together, these APIs provide the bridge without an intermediate image upload. Graphics Contexts, MTLBuffer.contents(), MTLTexture.
The example explicitly requires an Apple-family GPU. Apple’s current storage documentation says shared textures are unavailable on non-Apple-family GPUs. Treat GPU support as a prerequisite rather than assuming every Metal-capable Mac supports this exact allocation strategy. MTLStorageMode.shared.
| Your requirement | Appropriate approach |
|---|---|
| Core Graphics draws; Metal reads the same allocation on Apple silicon | Shared buffer plus buffer-backed texture, as below |
| A separately allocated texture should receive the drawing | Upload with replace(region:mipmapLevel:withBytes:bytesPerRow:) into a supported CPU-accessible texture |
| Repeated GPU use benefits from a separate private texture | Copy from a staging resource with a blit encoder |
| The selected GPU cannot create shared textures | Use a supported upload or staging-copy path instead of this shared-texture example |
These alternatives have different ownership and copying behavior. Apple documents replace as an immediate CPU copy, while blit encoders move data between resources, including CPU-accessible and private storage. The direct-sharing path avoids that upload copy, but Apple cautions that buffer-backed textures may receive fewer texture optimizations. Replacing texture pixels, MTLBlitCommandEncoder, Buffer-backed texture restrictions.
Define the pixel, size, and ownership contracts
Use an explicit byte layout: four bytes per pixel in increasing-address order R, G, B, A. For Core Graphics, combine premultipliedLast with byteOrder32Big; for Metal, use rgba8Unorm. The byte-order setting makes the memory interpretation explicit, independent of how a native integer would be represented. Apple documents the alpha position, byte-order controls, and Metal’s four normalized RGBA components separately. CGImageAlphaInfo.premultipliedLast, CGBitmapInfo, MTLPixelFormat.rgba8Unorm.
Premultiplied alpha means the stored color channels already include alpha. As an illustrative calculation, half-transparent full red over transparent storage is approximately 128, 0, 0, 128, allowing for quantization. A downstream shader must preserve that interpretation; multiplying the sampled RGB by alpha again would darken it. The example uses opaque shapes to keep its byte check straightforward. Bitmap Images and Image Masks.
The example chooses CGColorSpace.linearSRGB, which Apple defines as sRGB colorimetry with a linear transfer function. This establishes the drawing’s color interpretation explicitly; sharing memory itself performs no color-space conversion. Keep that interpretation consistent when integrating the texture into a renderer. CGColorSpace.linearSRGB.
Calculate the row stride before allocating
The visible row length is width × 4, but the allocated stride may be larger. Query minimumLinearTextureAlignment(for:) for the selected device and format, then round the visible row length upward to a multiple of that alignment. Give Core Graphics and Metal the same resulting bytesPerRow. minimumLinearTextureAlignment(for:).
For an explicitly illustrative device alignment of 256 bytes, a 257 × 128 image needs 1,028 visible bytes per row, a 1,280-byte aligned stride, and 163,840 allocated bytes. The extra 252 bytes per row are padding, not additional pixels. The program queries the real alignment instead of assuming this illustrative value.
Allocate at least bytesPerRow × height bytes. For a pixel at memory column x and row y, its byte offset is y × bytesPerRow + x × 4. When adapting the fixed example to variable dimensions, reject nonpositive sizes and check multiplication and rounding for integer overflow before allocation. Core Graphics documents the full stride-times-height storage requirement. Creating a Bitmap Graphics Context.
Keep the allocation alive through every consumer
In this design, Metal allocates the memory. The bitmap context borrows its address; keep the buffer alive for the context’s entire lifetime. For asynchronous GPU work, also retain the resources until that work completes. Do not free buffer.contents() yourself or substitute a pointer obtained from a temporary Swift array access.
The program finishes and releases its drawing context before submitting GPU work, then keeps the buffers and texture available until completion. This follows the caller-supplied bitmap-memory model and Metal’s documented CPU/GPU ordering requirements. Graphics Contexts, Synchronizing CPU and GPU work.
Build a complete synthetic drawing example
Create a macOS command-line project, or save the following complete program as main.swift. Use a current Xcode macOS SDK: the code uses the current CGContext initializer whose bitmapInfo argument is a CGBitmapInfo. Older SDKs may expose the legacy initializer expecting bitmapInfo.rawValue instead. CGContext bitmap initializer.
The drawing combines direct Core Graphics calls with an AppKit oval. NSGraphicsContext(cgContext:flipped:) wraps the existing context, so both draw into the same destination. Saving and restoring AppKit’s graphics state restores the previous current context after drawing. NSGraphicsContext initializer, saveGraphicsState(), restoreGraphicsState().
import AppKit
import CoreGraphics
import Metal
// Fixed, small dimensions keep all size arithmetic bounded.
let width = 257
let height = 128
let format: MTLPixelFormat = .rgba8Unorm
guard let device = MTLCreateSystemDefaultDevice(),
device.supportsFamily(.apple1) else {
fatalError("This example requires an Apple-family GPU.")
}
let alignment = device.minimumLinearTextureAlignment(for: format)
guard alignment > 0 else { fatalError("Invalid alignment.") }
let rowBytes = ((width * 4 + alignment - 1) / alignment) * alignment
let byteCount = rowBytes * height
guard let buffer = device.makeBuffer(
length: byteCount, options: .storageModeShared
) else { fatalError("Drawing buffer allocation failed.") }
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: format, width: width, height: height, mipmapped: false
)
descriptor.storageMode = .shared
descriptor.usage = .shaderRead
guard let texture = buffer.makeTexture(
descriptor: descriptor, offset: 0, bytesPerRow: rowBytes
) else { fatalError("Buffer-backed texture creation failed.") }
precondition(texture.buffer === buffer)
// This scope finishes CPU drawing before GPU submission.
do {
guard let space = CGColorSpace(name: CGColorSpace.linearSRGB) else {
fatalError("Color space creation failed.")
}
let info = CGBitmapInfo(
rawValue: CGImageAlphaInfo.premultipliedLast.rawValue
).union(.byteOrder32Big)
guard let cg = CGContext(
data: buffer.contents(), width: width, height: height,
bitsPerComponent: 8, bytesPerRow: rowBytes,
space: space, bitmapInfo: info
) else { fatalError("Bitmap context creation failed.") }
cg.setFillColor(red: 1, green: 0, blue: 0, alpha: 1)
cg.fill(CGRect(x: 0, y: 0,
width: CGFloat(width), height: CGFloat(height)))
NSGraphicsContext.saveGraphicsState()
defer { NSGraphicsContext.restoreGraphicsState() }
NSGraphicsContext.current = NSGraphicsContext(
cgContext: cg, flipped: false
)
NSColor.blue.setFill()
NSBezierPath(ovalIn: NSRect(x: 32, y: 24, width: 48, height: 48)).fill()
}
// Diagnostic copy: prove the GPU can read through the texture.
guard let readback = device.makeBuffer(
length: byteCount, options: .storageModeShared
), let queue = device.makeCommandQueue(),
let command = queue.makeCommandBuffer(),
let blit = command.makeBlitCommandEncoder() else {
fatalError("Readback setup failed.")
}
blit.copy(
from: texture, sourceSlice: 0, sourceLevel: 0,
sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0),
sourceSize: MTLSize(width: width, height: height, depth: 1),
to: readback, destinationOffset: 0,
destinationBytesPerRow: rowBytes, destinationBytesPerImage: 0
)
blit.endEncoding()
command.commit()
command.waitUntilCompleted()
guard command.status == .completed else {
fatalError("GPU copy failed: \(String(describing: command.error))")
}
let pixel = readback.contents().assumingMemoryBound(to: UInt8.self)
precondition(pixel[0] == 255 && pixel[1] == 0 &&
pixel[2] == 0 && pixel[3] == 255)
withExtendedLifetime(buffer) {
print("Shared RGBA texture verified; row stride: \(rowBytes) bytes")
}
Compile and run it on the target Mac:
xcrun swiftc main.swift -o SharedRGBA -framework AppKit -framework CoreGraphics -framework Metal
./SharedRGBA
The texture’s buffer property checks its backing resource. The diagnostic then copies texture pixels into a separate shared buffer and checks an untouched red corner after GPU completion. That readback is an intentional test copy; it is separate from the shared drawing allocation. Apple documents the texture-to-buffer operation and specifies destinationBytesPerImage: 0 for a 2D copy. MTLTexture.buffer, Texture-to-buffer copy.
The stride calculations were checked with deterministic sample inputs. The Swift program was inspected against the retrieved APIs but could not be compiled or run in the research environment, which provides Linux without Swift or Apple frameworks.
Coordinate CPU drawing and GPU use
Shared storage still requires an access schedule. Finish CPU drawing before committing commands that read the texture. After submission, leave that allocation unchanged until the GPU finishes using it. Apple explicitly requires this coordination for shared resources. MTLStorageMode.shared.
For the one-shot example, waitUntilCompleted() makes the handoff easy to inspect: it blocks until the command buffer and its completion handlers finish. In an interactive renderer, avoid placing that blocking wait in every frame. Instead, maintain multiple buffer-and-texture pairs and return each pair to the available pool only after its GPU completion notification. Apple demonstrates this resource-pool pattern in its synchronization sample. waitUntilCompleted(), Synchronizing CPU and GPU work.
If you later make the GPU write through one alias and read through another, review the synchronization again. Apple notes that buffer-backed texture data can be coherent between render passes without immediately reflecting alias changes inside one pass. The example avoids that complication by completing CPU drawing before its single GPU read operation. Buffer-backed texture coherence.
Diagnose layout problems before changing the renderer
Use the following sequence to isolate the contract that failed. These checks follow from the documented pixel layout, backing-resource, stride, and synchronization rules above.
- Texture creation fails: confirm the GPU supports shared textures, both resources use
.shared, and the offset and stride meet the queried alignment. Keep the texture 2D, single-sample, with one mip level and one array element. - Red appears blue: inspect individual bytes. The example requires RGBA byte order,
premultipliedLast,byteOrder32Big, and.rgba8Unormtogether. - Rows appear skewed: compare the context and texture strides. Both must use
rowBytes, including padding. - The image appears inverted: check the drawing transform and the renderer’s texture-coordinate mapping. The example uses an unflipped drawing context. AppKit’s
flippedargument describes its initial flipped state; it does not rearrange the stored rows. - Updates appear inconsistently: check whether CPU drawing overlaps GPU access to the same allocation. Use completion tracking before reusing it.
The creation constraints come from Apple’s linear-texture documentation, and the flipped-state distinction comes from the AppKit initializer. Keep orientation handling explicit when adding your own display shader. Linear-texture requirements, AppKit flipped-state parameter.
Once the backing buffer, byte layout, padded stride, and access schedule agree, the synthetic drawing is available to Metal through the shared texture. Preserve those contracts when resizing the image or adding more drawing operations; choose a staging copy when the target GPU or downstream texture requirements call for separate storage.
Sources
MTLTexture — Apple
MTLStorageMode.shared — Apple
contents() — Apple
Graphics Contexts — Apple
Bitmap Images and Image Masks — Apple
CGBitmapInfo — Apple
MTLPixelFormat.rgba8Unorm — Apple
linearSRGB — Apple
init(data:width:height:bitsPerComponent:bytesPerRow:space:bitmapInfo:) — Apple
init(cgContext:flipped:) — Apple
saveGraphicsState() — Apple
restoreGraphicsState() — Apple
buffer — Apple
Synchronizing CPU and GPU work — Apple
waitUntilCompleted() — Apple
MTLBlitCommandEncoder — Apple