Zeichnen Sie auf einem Apple-Silicon-Mac über einen Bitmap-CGContext in einen gemeinsam genutzten MTLBuffer und erstellen Sie anschließend eine Metal-Textur, die von demselben Puffer unterstützt wird. Entscheidend sind übereinstimmende RGBA-Bytes, ausgerichtete Zeilenabstände, beibehaltener Speicher und geordneter CPU-/GPU-Zugriff.

Diese Anleitung erstellt ein synthetisches rotes Rechteck und ein blaues Oval und prüft anschließend, ob Metal die Zeichnung über die Textur lesen kann. Sie richtet sich an Mac-Entwickler, die generierte Grafiken für den Metal-Renderer einer Desktop-App vorbereiten. Apple dokumentiert den Freigabemechanismus in der API makeTexture(descriptor:offset:bytesPerRow:).

Welchen Freigabepfad sollten Sie wählen?

Weisen Sie den Speicher zuerst mit Metal zu. Rufen Sie dessen CPU-Adresse mit buffer.contents() ab, verwenden Sie diese Adresse als Ziel des Bitmap-Kontexts und erstellen Sie die Textur aus dem Puffer. Apple dokumentiert, dass Bitmap-Zeichnungen den bereitgestellten Speicher aktualisieren und dass eine pufferbasierte Textur die Speicherzuweisung ihres Puffers teilt. Zusammen bieten diese APIs die Verbindung ohne einen zwischengeschalteten Bild-Upload. Grafikkontexte, MTLBuffer.contents(), MTLTexture.

Das Beispiel setzt ausdrücklich eine Apple-GPU voraus. Apples aktuelle Dokumentation zu Speicherarten besagt, dass gemeinsam genutzte Texturen auf GPUs außerhalb der Apple-Familie nicht verfügbar sind. Betrachten Sie die GPU-Unterstützung als Voraussetzung, statt anzunehmen, jeder Metal-fähige Mac unterstütze genau diese Zuweisungsstrategie. MTLStorageMode.shared.

Ihre Anforderung Geeigneter Ansatz
Core Graphics zeichnet; Metal liest auf Apple Silicon dieselbe Speicherzuweisung Gemeinsam genutzter Puffer plus pufferbasierte Textur wie unten
Eine separat zugewiesene Textur soll die Zeichnung erhalten Mit replace(region:mipmapLevel:withBytes:bytesPerRow:) in eine unterstützte CPU-zugängliche Textur hochladen
Wiederholte GPU-Nutzung profitiert von einer separaten privaten Textur Mit einem Blit-Encoder aus einer Staging-Ressource kopieren
Die ausgewählte GPU kann keine gemeinsam genutzten Texturen erstellen Statt dieses Beispiels mit gemeinsam genutzter Textur einen unterstützten Upload- oder Staging-Kopierpfad verwenden

Diese Alternativen unterscheiden sich bei Besitzverhältnissen und Kopierverhalten. Apple beschreibt replace als sofortige CPU-Kopie, während Blit-Encoder Daten zwischen Ressourcen verschieben, auch zwischen CPU-zugänglichem und privatem Speicher. Der direkte Freigabepfad vermeidet diese Upload-Kopie, doch Apple weist darauf hin, dass pufferbasierte Texturen möglicherweise weniger Texturoptimierungen erhalten. Texturpixel ersetzen, MTLBlitCommandEncoder, Einschränkungen pufferbasierter Texturen.

Pixel-, Größen- und Besitzverträge festlegen

Verwenden Sie ein explizites Byte-Layout: vier Bytes pro Pixel in der Reihenfolge steigender Speicheradressen R, G, B, A. Kombinieren Sie für Core Graphics premultipliedLast mit byteOrder32Big; verwenden Sie für Metal rgba8Unorm. Die Byte-Reihenfolge-Einstellung macht die Interpretation des Speichers explizit und unabhängig davon, wie eine native Ganzzahl dargestellt würde. Apple dokumentiert die Alpha-Position, die Byte-Reihenfolge-Steuerung und Metals vier normalisierte RGBA-Komponenten separat. CGImageAlphaInfo.premultipliedLast, CGBitmapInfo, MTLPixelFormat.rgba8Unorm.

Bei vormultipliziertem Alpha enthalten die gespeicherten Farbkanäle bereits den Alpha-Wert. Als beispielhafte Berechnung ergibt halbtransparentes Vollrot über transparentem Speicher ungefähr 128, 0, 0, 128, abhängig von der Quantisierung. Ein nachgelagerter Shader muss diese Interpretation beibehalten; wenn er das abgetastete RGB erneut mit Alpha multipliziert, wird das Bild dunkler. Das Beispiel verwendet deckende Formen, damit die Byte-Prüfung einfach bleibt. Bitmapbilder und Bildmasken.

Das Beispiel wählt CGColorSpace.linearSRGB, das Apple als sRGB-Farbmetrik mit linearer Übertragungsfunktion definiert. Dadurch wird die Farbinterpretation der Zeichnung explizit festgelegt; das Teilen des Speichers selbst führt keine Farbraumkonvertierung durch. Achten Sie bei der Einbindung der Textur in einen Renderer auf eine einheitliche Interpretation. CGColorSpace.linearSRGB.

Zeilenabstand vor der Zuweisung berechnen

Die sichtbare Zeilenlänge beträgt width × 4, der zugewiesene Zeilenabstand kann jedoch größer sein. Fragen Sie minimumLinearTextureAlignment(for:) für das ausgewählte Gerät und Format ab und runden Sie dann die sichtbare Zeilenlänge auf das nächste Vielfache dieser Ausrichtung auf. Übergeben Sie Core Graphics und Metal denselben resultierenden Wert für bytesPerRow. minimumLinearTextureAlignment(for:).

Bei einer ausdrücklich nur beispielhaften Geräteausrichtung von 256 Bytes benötigt ein Bild mit den Maßen 257 × 128 pro Zeile 1,028 sichtbare Bytes, einen ausgerichteten Zeilenabstand von 1,280 Bytes und insgesamt 163,840 zugewiesene Bytes. Die zusätzlichen 252 Bytes pro Zeile sind Auffüllung und keine weiteren Pixel. Das Programm fragt die tatsächliche Ausrichtung ab, statt diesen Beispielwert vorauszusetzen.

Weisen Sie mindestens bytesPerRow × height Bytes zu. Für ein Pixel an der Speicherposition Spalte x und Zeile y beträgt der Byte-Offset y × bytesPerRow + x × 4. Wenn Sie das feste Beispiel an variable Abmessungen anpassen, weisen Sie nicht positive Größen zurück und prüfen Sie vor der Zuweisung, ob Multiplikation und Aufrundung einen Ganzzahlüberlauf verursachen. Core Graphics dokumentiert den vollständigen Speicherbedarf aus Zeilenabstand mal Höhe. Einen Bitmap-Grafikkontext erstellen.

Die Zuweisung während der Nutzung am Leben halten

Bei diesem Entwurf weist Metal den Speicher zu. Der Bitmap-Kontext leiht sich dessen Adresse; halten Sie den Puffer während der gesamten Lebensdauer des Kontexts am Leben. Bei asynchroner GPU-Arbeit müssen Sie die Ressourcen außerdem behalten, bis diese Arbeit abgeschlossen ist. Geben Sie buffer.contents() nicht selbst frei und ersetzen Sie ihn nicht durch einen Zeiger, der aus dem Zugriff auf ein temporäres Swift-Array stammt.

Das Programm schließt den Zeichenkontext und gibt ihn frei, bevor es GPU-Arbeit einreicht, und hält die Puffer und die Textur bis zum Abschluss verfügbar. Dies entspricht dem Modell für Bitmap-Speicher, der vom Aufrufer bereitgestellt wird, sowie den dokumentierten Anforderungen von Metal zur CPU-/GPU-Reihenfolge. Grafikkontexte, CPU- und GPU-Arbeit synchronisieren.

Ein vollständiges synthetisches Zeichenbeispiel erstellen

Erstellen Sie ein macOS-Befehlszeilenprojekt oder speichern Sie das folgende vollständige Programm als main.swift. Verwenden Sie ein aktuelles macOS-SDK von Xcode: Der Code verwendet den aktuellen CGContext-Initialisierer, dessen Argument bitmapInfo vom Typ CGBitmapInfo ist. Ältere SDKs bieten möglicherweise noch den veralteten Initialisierer, der stattdessen bitmapInfo.rawValue erwartet. CGContext-Bitmap-Initialisierer.

Die Zeichnung kombiniert direkte Core-Graphics-Aufrufe mit einem AppKit-Oval. NSGraphicsContext(cgContext:flipped:) umschließt den vorhandenen Kontext, sodass beide in dasselbe Ziel zeichnen. Das Sichern und Wiederherstellen des AppKit-Grafikzustands stellt nach dem Zeichnen den vorherigen aktuellen Kontext wieder her. NSGraphicsContext-Initialisierer, 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")
}

Kompilieren Sie das Programm und führen Sie es auf dem Ziel-Mac aus:

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

Die Eigenschaft buffer der Textur prüft deren zugrunde liegende Ressource. Anschließend kopiert die Diagnose die Texturpixel in einen separaten gemeinsam genutzten Puffer und prüft nach Abschluss der GPU-Arbeit eine unberührte rote Ecke. Dieser Rücklesevorgang ist eine beabsichtigte Testkopie und erfolgt getrennt von der gemeinsam genutzten Zeichnungszuweisung. Apple dokumentiert den Kopiervorgang von der Textur in einen Puffer und gibt für eine 2D-Kopie destinationBytesPerImage: 0 vor. MTLTexture.buffer, Textur-zu-Puffer-Kopie.

Die Zeilenabstandsberechnungen wurden mit deterministischen Beispielwerten geprüft. Das Swift-Programm wurde anhand der abgerufenen APIs geprüft, konnte jedoch in der Forschungsumgebung nicht kompiliert oder ausgeführt werden, da diese Linux ohne Swift oder Apple-Frameworks bereitstellt.

CPU-Zeichnen und GPU-Nutzung koordinieren

Gemeinsam genutzter Speicher erfordert weiterhin einen Zugriffsplan. Schließen Sie das CPU-Zeichnen ab, bevor Sie Befehle einreichen, die die Textur lesen. Ändern Sie nach dem Einreichen diese Zuweisung nicht, bis die GPU sie nicht mehr verwendet. Apple verlangt diese Koordination ausdrücklich für gemeinsam genutzte Ressourcen. MTLStorageMode.shared.

Beim einmaligen Beispiel vereinfacht waitUntilCompleted() die Übergabe, weil der Aufruf blockiert, bis der Befehlsbuffer und seine Abschluss-Handler fertig sind. Vermeiden Sie in einem interaktiven Renderer, in jedem Frame auf diese Weise zu blockieren. Halten Sie stattdessen mehrere Puffer- und Texturpaare vor und geben Sie jedes Paar erst nach der GPU-Abschlussmeldung wieder für den verfügbaren Pool frei. Apple zeigt dieses Ressourcenpool-Muster in seinem Synchronisierungsbeispiel. waitUntilCompleted(), CPU- und GPU-Arbeit synchronisieren.

Wenn Sie später über einen Alias in die GPU schreiben und über einen anderen lesen, prüfen Sie die Synchronisierung erneut. Apple weist darauf hin, dass Daten pufferbasierter Texturen zwischen Render-Pässen konsistent sein können, Änderungen durch Aliase innerhalb eines Passes jedoch nicht sofort sichtbar sind. Das Beispiel vermeidet diese Komplikation, indem es das CPU-Zeichnen vor dem einmaligen GPU-Lesevorgang abschließt. Kohärenz pufferbasierter Texturen.

Layoutprobleme diagnostizieren, bevor Sie den Renderer ändern

Gehen Sie wie folgt vor, um den fehlerhaften Vertrag einzugrenzen. Diese Prüfungen ergeben sich aus den oben beschriebenen Regeln zu Pixellayout, zugrunde liegender Ressource, Zeilenabstand und Synchronisierung.

  1. Texterstellung schlägt fehl: Prüfen Sie, ob die GPU gemeinsam genutzte Texturen unterstützt, beide Ressourcen .shared verwenden und Offset sowie Zeilenabstand die abgefragte Ausrichtung erfüllen. Die Textur muss zweidimensional sein, mit einem Sample, einer Mip-Stufe und einem Array-Element.
  2. Rot erscheint blau: Prüfen Sie die einzelnen Bytes. Das Beispiel erfordert gemeinsam RGBA-Byte-Reihenfolge, premultipliedLast, byteOrder32Big und .rgba8Unorm.
  3. Zeilen wirken verschoben: Vergleichen Sie den Zeilenabstand von Kontext und Textur. Beide müssen rowBytes verwenden, einschließlich der Auffüllung.
  4. Das Bild erscheint auf dem Kopf: Prüfen Sie die Zeichentransformation und die Texturkoordinatenzuordnung des Renderers. Das Beispiel verwendet einen nicht gespiegelten Zeichenkontext. Das Argument flipped von AppKit beschreibt dessen anfänglichen Spiegelungszustand; es ordnet die gespeicherten Zeilen nicht neu an.
  5. Aktualisierungen erscheinen uneinheitlich: Prüfen Sie, ob CPU-Zeichnen und GPU-Zugriff gleichzeitig auf dieselbe Zuweisung zugreifen. Verfolgen Sie den Abschluss, bevor Sie sie erneut verwenden.

Die Erstellungsanforderungen ergeben sich aus Apples Dokumentation zu linearen Texturen; die Unterscheidung beim Spiegelungszustand stammt vom AppKit-Initialisierer. Legen Sie die Behandlung der Ausrichtung explizit fest, wenn Sie einen eigenen Anzeigeshader hinzufügen. Anforderungen an lineare Texturen, AppKit-Parameter für den Spiegelungszustand.

Sobald zugrunde liegender Puffer, Byte-Layout, aufgefüllter Zeilenabstand und Zugriffsplan übereinstimmen, kann Metal über die gemeinsam genutzte Textur auf die synthetische Zeichnung zugreifen. Behalten Sie diese Verträge beim Ändern der Bildgröße oder beim Hinzufügen weiterer Zeichenoperationen bei; wählen Sie eine Staging-Kopie, wenn die Ziel-GPU oder die Anforderungen der nachgelagerten Textur einen separaten Speicher erfordern.

Quellen