Apple 실리콘 Mac에서 비트맵 CGContext를 통해 공유 MTLBuffer에 그린 다음, 동일한 버퍼를 기반으로 Metal 텍스처를 만듭니다. 핵심 조건은 RGBA 바이트 구성의 일치, 정렬된 행 간격, 저장 공간의 유지, CPU/GPU 접근 순서입니다.

이 가이드에서는 합성 빨간 사각형과 파란 타원을 그리고, Metal이 텍스처를 통해 해당 드로잉을 읽을 수 있는지 확인합니다. 데스크톱 앱의 Metal 렌더러에 생성된 그래픽을 전달하려는 Mac 개발자를 위한 내용입니다. 공유 방식은 Apple의 makeTexture(descriptor:offset:bytesPerRow:) API에 문서화되어 있습니다.

어떤 공유 방식을 선택해야 할까요?

먼저 Metal로 저장 공간을 할당합니다. buffer.contents()을 사용해 CPU 주소를 얻고, 그 주소를 비트맵 컨텍스트의 대상으로 사용한 다음, 버퍼에서 텍스처를 만듭니다. Apple 문서에 따르면 비트맵 드로잉은 제공된 메모리를 업데이트하며, 버퍼 기반 텍스처는 버퍼의 할당 공간을 공유합니다. 이 API들을 함께 사용하면 중간 이미지 업로드 없이 두 API 사이를 연결할 수 있습니다. Graphics Contexts, MTLBuffer.contents(), MTLTexture.

이 예제는 Apple 계열 GPU를 명시적으로 요구합니다. Apple의 최신 저장 모드 문서에 따르면 Apple 계열이 아닌 GPU에서는 공유 텍스처를 사용할 수 없습니다. Metal을 지원하는 모든 Mac이 이 특정 할당 방식을 지원한다고 가정하지 말고, GPU 지원을 전제 조건으로 취급하세요. MTLStorageMode.shared.

요구 사항 적합한 방식
Core Graphics로 그리고 Apple 실리콘에서 Metal이 같은 할당 공간을 읽음 아래 예제처럼 공유 버퍼와 버퍼 기반 텍스처 사용
별도로 할당된 텍스처에 드로잉을 전달해야 함 지원되는 CPU 접근 가능 텍스처에 replace(region:mipmapLevel:withBytes:bytesPerRow:)로 업로드
GPU에서 반복 사용하므로 별도의 비공개 텍스처가 유리함 블릿 인코더로 스테이징 리소스에서 복사
선택한 GPU가 공유 텍스처를 만들 수 없음 이 공유 텍스처 예제 대신 지원되는 업로드 또는 스테이징 복사 방식 사용

이 방식들은 소유권과 복사 동작이 서로 다릅니다. Apple은 replace을 즉시 수행되는 CPU 복사로 문서화하며, 블릿 인코더는 CPU 접근 가능 저장 공간과 비공개 저장 공간을 포함해 리소스 간에 데이터를 옮깁니다. 직접 공유 방식은 업로드 복사를 피하지만, Apple은 버퍼 기반 텍스처에 텍스처 최적화가 덜 적용될 수 있다고 설명합니다. 텍스처 픽셀 교체, MTLBlitCommandEncoder, 버퍼 기반 텍스처의 제한 사항.

픽셀, 크기, 소유권 조건 정의하기

명시적인 바이트 레이아웃을 사용하세요. 주소가 증가하는 순서대로 픽셀당 4바이트, 즉 R, G, B, A입니다. Core Graphics에서는 premultipliedLast와 byteOrder32Big를 조합하고, Metal에서는 rgba8Unorm을 사용합니다. 바이트 순서 설정을 지정하면 네이티브 정수가 메모리에 저장되는 방식과 관계없이 메모리 해석이 명확해집니다. Apple은 알파 위치, 바이트 순서 제어, Metal의 정규화된 RGBA 구성 요소 네 개를 각각 문서화합니다. CGImageAlphaInfo.premultipliedLast, CGBitmapInfo, MTLPixelFormat.rgba8Unorm.

프리멀티플라이드 알파에서는 저장된 색상 채널에 이미 알파가 반영되어 있습니다. 계산을 보여주는 예로, 투명한 저장 공간 위에 그린 불투명도 50%의 완전한 빨간색은 양자화 오차를 고려하면 대략 128, 0, 0, 128가 됩니다. 후속 셰이더는 이 해석을 유지해야 합니다. 샘플링한 RGB에 알파를 다시 곱하면 색이 어두워집니다. 바이트 검사를 간단히 하기 위해 이 예제에서는 불투명 도형을 사용합니다. 비트맵 이미지 및 이미지 마스크.

이 예제에서는 Apple이 sRGB 색도와 선형 전달 함수를 사용한다고 정의한 CGColorSpace.linearSRGB를 선택합니다. 이를 통해 드로잉의 색상 해석을 명시적으로 지정합니다. 메모리를 공유하는 것만으로는 색 공간 변환이 수행되지 않습니다. 텍스처를 렌더러에 통합할 때 이 해석을 일관되게 유지하세요. CGColorSpace.linearSRGB.

할당 전에 행 간격 계산하기

표시되는 행의 길이는 width × 4이지만 할당된 행 간격은 더 클 수 있습니다. 선택한 장치와 형식에 대해 minimumLinearTextureAlignment(for:)을 조회한 다음, 표시되는 행 길이를 해당 정렬 단위의 배수로 올림합니다. 계산된 동일한 bytesPerRow 값을 Core Graphics와 Metal에 전달하세요. minimumLinearTextureAlignment(for:).

장치 정렬 단위가 256바이트라고 가정한 예시에서 257 × 128 이미지에는 행마다 표시 데이터에 1,028바이트가 필요하고, 정렬된 행 간격은 1,280바이트이며, 총 163,840바이트를 할당해야 합니다. 행마다 추가되는 252바이트는 픽셀이 아니라 패딩입니다. 프로그램은 이 예시 값을 가정하는 대신 실제 정렬 단위를 조회합니다.

최소한 bytesPerRow × height바이트를 할당하세요. 메모리의 열 x, 행 y에 있는 픽셀의 바이트 오프셋은 y × bytesPerRow + x × 4입니다. 고정된 예제를 다양한 크기로 변경할 때는 양수가 아닌 크기를 거부하고, 할당 전에 곱셈과 올림 연산에서 정수 오버플로가 발생하는지 확인하세요. Core Graphics 문서에는 전체 저장 공간 요구 사항이 행 간격과 높이의 곱으로 명시되어 있습니다. 비트맵 그래픽 컨텍스트 만들기.

모든 사용자가 끝날 때까지 할당 공간 유지하기

이 설계에서는 Metal이 메모리를 할당합니다. 비트맵 컨텍스트는 그 주소를 빌려 쓰므로 컨텍스트의 전체 수명 동안 버퍼를 유지해야 합니다. 비동기 GPU 작업의 경우 해당 작업이 완료될 때까지 리소스도 유지해야 합니다. buffer.contents()을 직접 해제하거나 임시 Swift 배열 접근에서 얻은 포인터로 대체하지 마세요.

프로그램은 GPU 작업을 제출하기 전에 드로잉 컨텍스트를 마치고 해제한 다음, 작업이 완료될 때까지 버퍼와 텍스처를 사용할 수 있도록 유지합니다. 이는 호출자가 제공한 비트맵 메모리 모델과 Apple이 문서화한 CPU/GPU 순서 지정 요구 사항을 따릅니다. Graphics Contexts, CPU 및 GPU 작업 동기화.

완전한 합성 드로잉 예제 만들기

macOS 명령줄 프로젝트를 만들거나 다음 전체 프로그램을 main.swift로 저장하세요. 최신 Xcode macOS SDK를 사용하세요. 이 코드는 bitmapInfo 인수가 CGBitmapInfo인 최신 CGContext 초기화자를 사용합니다. 이전 SDK에서는 대신 bitmapInfo.rawValue를 받는 레거시 초기화자만 제공할 수 있습니다. CGContext 비트맵 초기화자.

드로잉은 Core Graphics의 직접 호출과 AppKit 타원 그리기를 결합합니다. NSGraphicsContext(cgContext:flipped:)은 기존 컨텍스트를 감싸므로 두 방식 모두 같은 대상에 그립니다. AppKit 그래픽 상태를 저장하고 복원하면 드로잉 후 이전 현재 컨텍스트가 복구됩니다. NSGraphicsContext 초기화자, 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")
}

대상 Mac에서 컴파일하고 실행하세요.

xcrun swiftc main.swift -o SharedRGBA -framework AppKit -framework CoreGraphics -framework Metal
./SharedRGBA

텍스처의 buffer 속성으로 텍스처를 뒷받침하는 리소스를 확인합니다. 진단 코드는 GPU 작업이 완료된 뒤 텍스처 픽셀을 별도의 공유 버퍼로 복사하고, 손대지 않은 빨간색 모서리 픽셀을 확인합니다. 이 읽기 작업에는 의도적으로 복사가 포함되며, 공유 드로잉 할당 공간과는 별개입니다. Apple은 텍스처에서 버퍼로 복사하는 작업을 문서화하고, 2D 복사에서 destinationBytesPerImage: 0을 지정하도록 안내합니다. MTLTexture.buffer, 텍스처에서 버퍼로 복사.

행 간격 계산은 결정적인 샘플 입력으로 확인했습니다. Swift 프로그램은 조회한 API 문서와 대조해 검토했지만, 조사 환경이 Swift와 Apple 프레임워크가 없는 Linux이므로 컴파일하거나 실행할 수 없었습니다.

CPU 드로잉과 GPU 사용 순서 조정하기

공유 저장 공간을 사용해도 접근 순서는 정해야 합니다. 텍스처를 읽는 명령을 커밋하기 전에 CPU 드로잉을 마치세요. 명령을 제출한 다음에는 GPU가 해당 할당 공간을 사용하는 동안 그 내용을 변경하지 마세요. Apple은 공유 리소스에서 이런 조정을 명시적으로 요구합니다. MTLStorageMode.shared.

한 번 실행하는 예제에서는 waitUntilCompleted()을 사용해 핸드오프 과정을 쉽게 확인할 수 있습니다. 이 메서드는 명령 버퍼와 완료 핸들러가 끝날 때까지 대기합니다. 대화형 렌더러에서는 매 프레임마다 이 대기 메서드를 호출하지 마세요. 대신 버퍼와 텍스처 쌍을 여러 개 유지하고, GPU 완료 알림을 받은 뒤 각 쌍을 사용 가능 풀로 돌려보내세요. Apple은 동기화 샘플에서 이 리소스 풀 패턴을 보여 줍니다. waitUntilCompleted(), CPU 및 GPU 작업 동기화.

나중에 한 별칭을 통해 GPU가 쓰고 다른 별칭을 통해 읽도록 변경한다면 동기화 방식을 다시 검토하세요. Apple은 버퍼 기반 텍스처 데이터가 렌더 패스 간에는 일관성을 유지할 수 있지만, 단일 패스 내에서 별칭의 변경 내용이 즉시 반영되지는 않을 수 있다고 설명합니다. 이 예제에서는 한 번의 GPU 읽기 작업 전에 CPU 드로잉을 완료하므로 이 복잡성을 피합니다. 버퍼 기반 텍스처 일관성.

렌더러를 변경하기 전에 레이아웃 문제 진단하기

다음 순서에 따라 어떤 조건이 충족되지 않았는지 확인하세요. 이 검사 항목은 앞서 설명한 픽셀 레이아웃, 기반 리소스, 행 간격, 동기화 규칙을 따릅니다.

  1. 텍스처 생성 실패: GPU가 공유 텍스처를 지원하는지 확인하고, 두 리소스 모두 .shared를 사용하는지, 오프셋과 행 간격이 조회한 정렬 조건을 충족하는지 확인하세요. 텍스처는 2D, 단일 샘플, 밉 레벨 하나, 배열 요소 하나로 유지하세요.
  2. 빨간색이 파란색으로 표시됨: 개별 바이트를 확인하세요. 이 예제에서는 RGBA 바이트 순서, premultipliedLast, byteOrder32Big, .rgba8Unorm을 함께 사용해야 합니다.
  3. 행이 비스듬하게 표시됨: 컨텍스트와 텍스처의 행 간격을 비교하세요. 패딩을 포함해 둘 다 rowBytes을 사용해야 합니다.
  4. 이미지가 뒤집혀 표시됨: 드로잉 변환과 렌더러의 텍스처 좌표 매핑을 확인하세요. 이 예제에서는 뒤집히지 않은 드로잉 컨텍스트를 사용합니다. AppKit의 flipped 인수는 초기 뒤집힘 상태를 나타낼 뿐, 저장된 행의 순서를 바꾸지 않습니다.
  5. 업데이트가 일관되지 않게 표시됨: CPU 드로잉이 같은 할당 공간에 대한 GPU 접근과 겹치는지 확인하세요. 해당 공간을 재사용하기 전에 완료 여부를 추적하세요.

생성 제약 조건은 Apple의 선형 텍스처 문서에 근거하고, 뒤집힘 상태의 구분은 AppKit 초기화자 설명에 근거합니다. 자체 표시 셰이더를 추가할 때는 방향 처리를 명시하세요. 선형 텍스처 요구 사항, AppKit 뒤집힘 상태 매개변수.

기반 버퍼, 바이트 레이아웃, 패딩된 행 간격, 접근 순서가 모두 일치하면 공유 텍스처를 통해 합성 드로잉을 Metal에서 사용할 수 있습니다. 이미지 크기를 조정하거나 드로잉 작업을 추가할 때도 이 조건을 유지하세요. 대상 GPU나 후속 텍스처 요구 사항에 별도 저장 공간이 필요하면 스테이징 복사를 선택하세요.

출처