Sur un Mac doté d’une puce Apple, dessinez dans un MTLBuffer partagé au moyen d’un CGContext bitmap, puis créez une texture Metal adossée à ce même tampon. Les contrats essentiels sont la correspondance des octets RGBA, l’alignement du pas des lignes, la conservation du stockage et l’ordonnancement des accès CPU/GPU.
Ce guide crée un rectangle rouge et un ovale bleu synthétiques, puis vérifie que Metal peut lire le dessin via la texture. Il s’adresse aux développeurs Mac qui préparent des graphismes générés pour le moteur de rendu Metal d’une application de bureau. Le mécanisme de partage est documenté par l’API Apple makeTexture(descriptor:offset:bytesPerRow:).
Quelle méthode de partage choisir ?
Allouez d’abord le stockage avec Metal. Obtenez son adresse CPU avec buffer.contents(), utilisez cette adresse comme destination du contexte bitmap, puis créez la texture à partir du tampon. Apple indique que le dessin bitmap met à jour la mémoire fournie et qu’une texture adossée à un tampon partage l’allocation de ce tampon. Ensemble, ces API assurent la liaison sans téléversement d’image intermédiaire. Graphics Contexts, MTLBuffer.contents(), MTLTexture.
L’exemple exige explicitement un GPU de la famille Apple. La documentation actuelle d’Apple sur les modes de stockage indique que les textures partagées ne sont pas disponibles sur les GPU qui ne sont pas de la famille Apple. Considérez la prise en charge du GPU comme un prérequis, au lieu de supposer que tous les Mac compatibles avec Metal prennent en charge cette stratégie d’allocation précise. MTLStorageMode.shared.
| Votre besoin | Approche adaptée |
|---|---|
| Core Graphics dessine ; Metal lit la même allocation sur une puce Apple | Tampon partagé et texture adossée à ce tampon, comme ci-dessous |
| Une texture allouée séparément doit recevoir le dessin | Téléversez avec replace(region:mipmapLevel:withBytes:bytesPerRow:) vers une texture prise en charge et accessible au CPU |
| Des utilisations GPU répétées bénéficient d’une texture privée distincte | Copiez depuis une ressource intermédiaire avec un encodeur blit |
| Le GPU sélectionné ne peut pas créer de textures partagées | Utilisez une méthode de téléversement ou de copie intermédiaire prise en charge au lieu de cet exemple avec texture partagée |
Ces solutions diffèrent par leur comportement en matière de propriété et de copie. Apple décrit replace comme une copie immédiate effectuée par le CPU, tandis que les encodeurs blit déplacent des données entre les ressources, y compris entre le stockage accessible au CPU et le stockage privé. Le partage direct évite cette copie de téléversement, mais Apple avertit que les textures adossées à un tampon peuvent bénéficier de moins d’optimisations de texture. Replacing texture pixels, MTLBlitCommandEncoder, Buffer-backed texture restrictions.
Définir les contrats relatifs aux pixels, à la taille et à la propriété
Utilisez une disposition explicite des octets : quatre octets par pixel, dans l’ordre des adresses croissantes R, G, B, A. Pour Core Graphics, combinez premultipliedLast avec byteOrder32Big ; pour Metal, utilisez rgba8Unorm. Le paramètre d’ordre des octets rend explicite l’interprétation de la mémoire, indépendamment de la représentation d’un entier natif. Apple documente séparément la position de l’alpha, les contrôles d’ordre des octets et les quatre composantes RGBA normalisées de Metal. CGImageAlphaInfo.premultipliedLast, CGBitmapInfo, MTLPixelFormat.rgba8Unorm.
Avec l’alpha prémultiplié, les canaux de couleur stockés incluent déjà la valeur alpha. À titre d’exemple de calcul, un rouge pur à mi-transparence sur un stockage transparent donne approximativement 128, 0, 0, 128, en tenant compte de la quantification. Un shader en aval doit préserver cette interprétation ; multiplier à nouveau le RGB échantillonné par l’alpha l’assombrirait. L’exemple utilise des formes opaques pour simplifier la vérification des octets. Bitmap Images and Image Masks.
L’exemple choisit CGColorSpace.linearSRGB, défini par Apple comme une colorimétrie sRGB dotée d’une fonction de transfert linéaire. Cela définit explicitement l’interprétation des couleurs du dessin ; le partage de mémoire n’effectue aucune conversion d’espace colorimétrique. Veillez à conserver cette interprétation lorsque vous intégrez la texture à un moteur de rendu. CGColorSpace.linearSRGB.
Calculer le pas des lignes avant l’allocation
La longueur visible d’une ligne est width × 4, mais le pas alloué peut être supérieur. Interrogez minimumLinearTextureAlignment(for:) pour l’appareil et le format sélectionnés, puis arrondissez la longueur visible vers le haut au multiple de cet alignement. Fournissez à Core Graphics et à Metal le même bytesPerRow. minimumLinearTextureAlignment(for:).
Pour un alignement d’appareil de 256 octets, donné à titre d’exemple, une image de 257 × 128 nécessite 1,028 octets visibles par ligne, un pas aligné de 1,280 octets et 163,840 octets alloués. Les 252 octets supplémentaires par ligne sont du remplissage, pas des pixels supplémentaires. Le programme interroge l’alignement réel au lieu de supposer cette valeur illustrative.
Allouez au moins bytesPerRow × height octets. Pour un pixel situé à la colonne mémoire x et à la ligne y, son décalage en octets est y × bytesPerRow + x × 4. Si vous adaptez l’exemple à des dimensions variables, rejetez les tailles non positives et vérifiez les débordements d’entiers lors des multiplications et des arrondis avant l’allocation. Core Graphics documente l’exigence de stockage complète, égale au pas multiplié par la hauteur. Creating a Bitmap Graphics Context.
Maintenir l’allocation en vie pendant toute son utilisation
Dans cette conception, Metal alloue la mémoire. Le contexte bitmap emprunte son adresse ; gardez le tampon en vie pendant toute la durée de vie du contexte. Pour les opérations GPU asynchrones, conservez également les ressources jusqu’à la fin de ces opérations. Ne libérez pas vous-même buffer.contents() et ne le remplacez pas par un pointeur obtenu lors de l’accès temporaire à un tableau Swift.
Le programme termine et libère le contexte de dessin avant de soumettre le travail GPU, puis garde les tampons et la texture disponibles jusqu’à la fin de ce travail. Cela respecte le modèle de mémoire bitmap fournie par l’appelant et les exigences documentées par Metal concernant l’ordonnancement CPU/GPU. Graphics Contexts, Synchronizing CPU and GPU work.
Construire un exemple complet de dessin synthétique
Créez un projet macOS en ligne de commande, ou enregistrez le programme complet ci-dessous sous le nom main.swift. Utilisez un SDK macOS récent de Xcode : le code emploie l’initialiseur CGContext actuel, dont l’argument bitmapInfo est un CGBitmapInfo. Les anciens SDK peuvent exposer l’ancien initialiseur qui attend plutôt bitmapInfo.rawValue. CGContext bitmap initializer.
Le dessin combine des appels directs à Core Graphics et un ovale AppKit. NSGraphicsContext(cgContext:flipped:) enveloppe le contexte existant : les deux dessinent donc sur la même destination. L’enregistrement puis la restauration de l’état graphique d’AppKit rétablit le contexte courant précédent après le dessin. 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")
}
Compilez et exécutez le programme sur le Mac cible :
xcrun swiftc main.swift -o SharedRGBA -framework AppKit -framework CoreGraphics -framework Metal
./SharedRGBA
La propriété buffer de la texture vérifie sa ressource sous-jacente. Le diagnostic copie ensuite les pixels de la texture dans un tampon partagé distinct et vérifie un coin rouge qui n’a pas été modifié, après la fin du travail GPU. Cette lecture avec copie est un test délibéré, distinct de l’allocation de dessin partagée. Apple documente la copie de texture vers tampon et précise que destinationBytesPerImage: 0 doit être utilisé pour une copie 2D. MTLTexture.buffer, Texture-to-buffer copy.
Les calculs de pas ont été vérifiés avec des entrées d’exemple déterministes. Le programme Swift a été examiné par rapport aux API consultées, mais n’a pas pu être compilé ni exécuté dans l’environnement de recherche, qui fonctionne sous Linux sans Swift ni frameworks Apple.
Coordonner le dessin CPU et l’utilisation GPU
Même avec un stockage partagé, il faut ordonnancer les accès. Terminez le dessin CPU avant de valider les commandes qui lisent la texture. Après leur soumission, ne modifiez plus cette allocation tant que le GPU l’utilise. Apple exige explicitement cette coordination pour les ressources partagées. MTLStorageMode.shared.
Pour cet exemple ponctuel, waitUntilCompleted() facilite l’examen du transfert : il bloque jusqu’à la fin du tampon de commandes et de ses gestionnaires de fin d’exécution. Dans un moteur de rendu interactif, évitez de bloquer ainsi à chaque image. Gérez plutôt plusieurs paires de tampons et de textures, et ne remettez une paire dans le pool disponible qu’après la notification de fin d’exécution GPU. Apple montre ce modèle de pool de ressources dans son exemple de synchronisation. waitUntilCompleted(), Synchronizing CPU and GPU work.
Si vous faites ensuite écrire le GPU via un alias et lire les données via un autre, réexaminez la synchronisation. Apple indique que les données d’une texture adossée à un tampon peuvent être cohérentes entre les passes de rendu sans refléter immédiatement les changements d’alias au cours d’une même passe. L’exemple évite cette complication en terminant le dessin CPU avant son unique opération de lecture GPU. Buffer-backed texture coherence.
Diagnostiquer les problèmes de disposition avant de modifier le moteur de rendu
Suivez la séquence ci-dessous pour isoler le contrat qui pose problème. Ces vérifications découlent des règles documentées ci-dessus concernant la disposition des pixels, la ressource sous-jacente, le pas des lignes et la synchronisation.
- La création de la texture échoue : vérifiez que le GPU prend en charge les textures partagées, que les deux ressources utilisent
.shared, et que le décalage et le pas respectent l’alignement interrogé. Gardez une texture 2D, à échantillonnage unique, avec un niveau de mipmap et un élément de tableau. - Le rouge apparaît bleu : inspectez les octets individuellement. L’exemple exige ensemble l’ordre des octets RGBA,
premultipliedLast,byteOrder32Biget.rgba8Unorm. - Les lignes semblent inclinées : comparez les pas du contexte et de la texture. Tous deux doivent utiliser
rowBytes, remplissage compris. - L’image apparaît inversée : vérifiez la transformation de dessin et le mappage des coordonnées de texture du moteur de rendu. L’exemple utilise un contexte de dessin sans inversion. L’argument
flippedd’AppKit indique son état initial d’inversion ; il ne réorganise pas les lignes stockées. - Les mises à jour semblent incohérentes : vérifiez si le dessin CPU chevauche l’accès GPU à la même allocation. Suivez la fin des opérations avant de réutiliser celle-ci.
Les contraintes de création sont tirées de la documentation d’Apple sur les textures linéaires, et la distinction concernant l’état d’inversion provient de l’initialiseur AppKit. Gérez explicitement l’orientation lorsque vous ajoutez votre propre shader d’affichage. Linear-texture requirements, AppKit flipped-state parameter.
Lorsque le tampon sous-jacent, la disposition des octets, le pas avec remplissage et l’ordonnancement des accès concordent, Metal peut accéder au dessin synthétique via la texture partagée. Respectez ces contrats lors du redimensionnement de l’image ou de l’ajout d’opérations de dessin ; choisissez une copie intermédiaire lorsque le GPU cible ou les exigences de la texture en aval imposent un stockage distinct.
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