Texture, formats de pixel — mémoire et précision
Texture de profondeur, Depth & Stencil — en détail
La texture de profondeur (depth buffer ou z-buffer) stocke la distance normalisée [0, 1] de chaque fragment au plan proche (near). Le GPU l'utilise pour le depth test : si un fragment entrant est plus loin qu'un fragment déjà écrit, il est rejeté avant le fragment shader — c'est l'early-z, l'optimisation fondamentale sur Apple Silicon. Cette texture est nécessaire pour superpositionner deux shaders, stocker l'endroit où se situe le curseur et avec une coordonnée de profondeur, par exemple.
Elle est à déclarer UNE fois et dans notre classe principale comme suit :
/// C++ — Renderer.hpp — update:18/05/26
private:
void makeAndResizeDepthTexture(NS::UInteger width, NS::UInteger height);
NS::SharedPtr<MTL::Texture> m_depth;
Créer la texture de profondeur
La classe MTL::TextureDescriptor est l’objet fourni par l'API qui la décrit. Les Setters de cette classe décrivent la texture finale stockée dans le pointeur intelligent m_depth :
/// C++ — Renderer.cpp — update:18/05/26
void Renderer::makeAndResizeDepthTexture(NS::UInteger width, NS::UInteger height)
{
auto _depthTextureDescriptor = NS::TransferPtr(MTL::TextureDescriptor::alloc()->init());
_depthTextureDescriptor->setStorageMode(MTL::StorageModePrivate);
_depthTextureDescriptor->setUsage(MTL::TextureUsageRenderTarget | MTL::TextureUsageShaderRead);
_depthTextureDescriptor->setTextureType(MTL::TextureType2D);
_depthTextureDescriptor->setPixelFormat(m_depthPixelFormat);
_depthTextureDescriptor->setWidth(width);
_depthTextureDescriptor->setHeight(height);
m_depth = NS::TransferPtr(m_device->newTexture(_depthTextureDescriptor.get()));
}
auto remplace ici NS::SharedPtr<MTL::TextureDescriptor>
Le pixel format détermine la précision disponible — un format trop petit introduit du banding, trop grand gaspille de la bande passante.
Mode de stockage setStorageMode()
MTL::StorageModeShared // CPU + GPU — accès CPU direct (replaceRegion, map)
Shared → à utiliser pour les textures uploadées depuis le CPU (atlas, polices, données initiales).
Une fois le contenu stable, migrer en Private via un blit encoder pour de meilleures performances GPU.
MTL::StorageModePrivate // GPU uniquement — VRAM optimisée, pas d'accès CPU
Private → mode par défaut pour tous les render targets, depth buffers, et textures de rendu.
Le CPU ne peut pas y accéder directement — une copie via blit encoder est nécessaire pour lire le résultat.
MTL::StorageModeMemoryless // tile memory uniquement — iOS/Apple Silicon
Memoryless → exclusif Apple Silicon (tile-based deferred rendering).
La texture n'existe que dans la mémoire interne des tiles durant la passe de rendu — elle n'est jamais écrite en VRAM.
Parfait pour la depth texture et les G-buffers intermédiaires sur iOS, économise toute la bande passante d'écriture.
Usage flags setUsage()
MTL::TextureUsageShaderRead
ShaderRead → sampling dans un shader.
MTL::TextureUsageShaderWrite
ShaderWrite → écriture depuis un compute shader.
MTL::TextureUsageRenderTarget
RenderTarget → attachement comme color/depth target.
Déclarer uniquement le/LES usages nécessaires — Metal peut activer des optimisations internes (compression lossless, fast-clear) uniquement si les flags sont précis. Une texture RenderTarget uniquement peut bénéficier du Delta Color Compression (DCC) sur Apple Silicon — réduction de bande passante automatique, invisible côté code.
Elle prend toute la surface de la fenêtre, peu importe sa taille et sera redimensionnée en fonction :
/// C++ — Renderer.cpp — update:18/05/26
void Renderer::resizeViewWindow(NS::UInteger width, NS::UInteger height)
{
m_windowSize = { (float)width, (float)height };
m_windowUniforms.windowSize.x = (float)width;
m_windowUniforms.windowSize.y = (float)height;
memcpy(m_windowSizeBuffer->contents(), &m_windowUniforms, sizeof(WindowUniforms));
updateViewportWindow(width, height);
m_camera.setAspectRatio((float)((float)width / (float)height));
makeAndResizeDepthTexture((float)width, (float)height); // ajoutes
}
Les Getters sont en bleu, contents() va chercher le contenu du buffer, on peut alors voir x provenant du float2 comme tel.
Créer et attacher un depth-stencil state au pipeline
MTL::DepthStencilState active le test : seuls les fragments plus proches que ce qui est déjà en buffer sont écrits.
/// C++ — Game2D.hpp — update:18/05/26 — draw()
private:
NS::SharedPtr<MTL::DepthStencilState> m_depthStencilState;
/// C++ — Game2D.cpp — update:18/05/26 — Constructeur ou fn dédiée
{
auto _depthStencilDescriptor = NS::TransferPtr(MTL::DepthStencilDescriptor::alloc()->init());
_depthStencilDescriptor->setDepthCompareFunction(MTL::CompareFunction::CompareFunctionLess);
_depthStencilDescriptor->setDepthWriteEnabled(true); // écriture du résultat
m_depthStencilState = NS::TransferPtr(device->newDepthStencilState(_depthStencilDescriptor.get()));
}
auto remplace ici NS::SharedPtr<MTL::DepthStencilDescriptor>
/// C++ — Game2D.cpp — update:18/05/26
renderCommandEncoder->setDepthStencilState(m_depthStencilState.get());
renderCommandEncoder->setRenderPipelineState(m_renderPipelineState.get());
Il est valide pour toutes les positions des vertices dans le shader Game2D.metal.
Pour comparer immédiatemment, tu en crées un pour la pipeline du fichier Shapes.metal :
/// C++ — Renderer.hpp — update:18/05/26
private:
NS::SharedPtr<MTL::DepthStencilState> m_depthStencilState;
/// C++ — Renderer.cpp — update:18/05/26 — Constructeur ou fn dédiée
{
auto _depthStencilDescriptor = NS::TransferPtr(MTL::DepthStencilDescriptor::alloc()->init());
_depthStencilDescriptor->setDepthCompareFunction(MTL::CompareFunction::CompareFunctionLess);
_depthStencilDescriptor->setDepthWriteEnabled(true); // écriture du résultat
m_depthStencilState = NS::TransferPtr(device->newDepthStencilState(_depthStencilDescriptor.get()));
}
/// C++ — Renderer.cpp — update:18/05/26 — draw()
renderCommandEncoder->setViewport(m_viewport);
renderCommandEncoder->setDepthStencilState(m_depthStencilState.get());
/// C++ — TextRendering.hpp — update:18/05/26
private:
NS::SharedPtr<MTL::DepthStencilState> m_depthStencilState;
/// C++ — TextRendering.cpp — update:18/05/26 — Constructeur ou fn dédiée
{
auto _depthStencilDescriptor = NS::TransferPtr(MTL::DepthStencilDescriptor::alloc()->init());
_depthStencilDescriptor->setDepthCompareFunction(MTL::CompareFunction::CompareFunctionAlways);
_depthStencilDescriptor->setDepthWriteEnabled(false); // pas d'écriture
m_depthStencilState = NS::TransferPtr(device->newDepthStencilState(_depthStencilDescriptor.get()));
}
/// C++ — TextRendering.cpp — update:18/05/26 — draw()
renderCommandEncoder->setRenderPipelineState(m_pipelineState.get());
renderCommandEncoder->setDepthStencilState(m_depthStencilState.get());
Les fonctions de comparaison setDepthCompareFunction()
En retouche photographique, tu utiliserais des calques, ici c'est un peu près la mpeme chose
MTL::CompareFunctionLess
Less → fragment visible si sa depth est strictement inférieure à celle du buffer. Standard pour une caméra dont le near est 0 et le far est 1.
MTL::CompareFunctionLessEqual
LessEqual → utile pour les passes de shadow (évite le z-fighting sur les surfaces coplanaires, triplanaires (triplanar mappping),…).
MTL::CompareFunctionGreater
Greater → rendu avec depth inversée (reverse-Z) — meilleure distribution de précision pour les grandes scènes.
MTL::CompareFunctionAlways
Always → passe toujours, utile pour le sky / background dessiné en dernier.
MTL::CompareFunctionNever
Never → rejette tout — utile pour des passes de stencil pures.
MTL::CompareFunctionNever
Never → rejette tout — utile pour des passes de stencil pures.
MTL::CompareFunctionNever
Never → rejette tout — utile pour des passes de stencil pures.
MTL::CompareFunctionNever
Never → rejette tout — utile pour des passes de stencil pures.
Attacher la depth texture au render pass
/// C++ — Renderer.cpp — update:18/05/26 — draw(), après avoir récupéré le renderPassDescriptor de la MTKView
renderPassDescriptor->colorAttachments()->object(0)->setClearColor(MTL::ClearColor(0.9, 0.9, 0.9, 1.0));
renderPassDescriptor->depthAttachment()->setTexture(m_depth.get());
renderPassDescriptor->depthAttachment()->setLoadAction(MTL::LoadActionClear);
renderPassDescriptor->depthAttachment()->setClearDepth(1.0f); // loin
renderPassDescriptor->depthAttachment()->setStoreAction(MTL::StoreActionStore);
Dans notre Game2D, le background est dessiné en dernier, ce qui écrase le soleil et le reste.
Sur les GPU tile-based (toute la gamme Apple Silicon), la depth texture n'a pas besoin d'être écrite en VRAM si elle n'est pas relue dans une passe suivante. Dans le cours, on en a besoin. StoreActionDontCare permet au GPU de simplement jeter les données de la tile — économie de bande passante significative.
Utilise StoreActionStore uniquement si tu relies la depth en compute ou dans une passe de post-processing (SSAO, shadow comparison, …).
La depth texture capture la scène, elle n'est pas projetée sur un mesh. Mais on peut tout à fait :
+ photoChoisir le bon Format de Pixel pour chaque utilisation
On le défini une fois : dans notre fichier .mm / Objective-C.
Après on ne fait que de créer un pointeur vers les formats.
Formats de pixel : la théorie
Un pixel format décrit comment chaque texel est encodé en mémoire GPU : combien de canaux, combien de bits par canal, et comment les interpréter (entier, flottant, normalisé…). Ce choix impacte directement la bande passante, la précision du rendu, et la compatibilité avec les étapes du pipeline.
Le chapitre sur les textures a présenté quelques formats courants — ici on décortique la logique complète, des noms jusqu'aux décisions d'architecture.
Anatomie d'un nom de format
MTL::PixelFormat RGBA 16 Float
│ │ └─ type de données
│ └──────── bits par canal
└──────────── canaux présents
Les types de données ;
- Unorm
→ Unsigned Normalized — entier non signé mappé sur [0.0, 1.0] dans le shader. Le format le plus courant pour les textures de couleur 8 bits. - Snorm
→ Signed Normalized — entier signé mappé sur [-1.0, 1.0]. Utile pour stocker des normales tangentes compressées. - Float
→ IEEE 754 — virgule flottante native. Précision maximale, indispensable pour l'HDR et la depth. Notre écran P3 peut afficher ce format de pixel. - Half
→ Flottant 16 bits (half precision). Bon compromis entre précision et bande passante pour les G-buffers. - Uint / Sint
→ Entiers non signés / signés bruts — pas de normalisation. Utilisés pour des données d'index, des masques, ou des buffers de visibilité. - Srgb (suffixe _sRGB)
→ Indique que Metal applique automatiquement la correction gamma à la lecture (décodage sRGB→linéaire) et à l'écriture (encodage linéaire→sRGB) sur les render targets. Ne jamais utiliser pour des données qui ne sont pas perceptuelles (normales, roughness, …).
Tableau complet — formats courants en Metal C++
| Format | Bits/texel | Type MSL | Usage typique |
|---|---|---|---|
RGBA8Unorm |
32 | float4 |
Normal map, roughness RGBA packée |
RGBA8Unorm_sRGB |
32 | float4 |
Albedo — correction gamma automatique |
RGBA16Float |
64 | half4 |
Render target HDR, G-Buffer, EDR |
RGBA32Float |
128 | float4 |
Compute intermédiaire, HDR extrême |
RG11B10Float |
32 | float3 / float4 |
HDR color sans alpha — même taille que RGBA8 |
RGB10A2Unorm |
32 | float4 |
Wide color, sortie Display P3 |
R8Unorm |
8 | float |
Roughness, metallic, masques |
RG8Unorm |
16 | float2 |
Normal map XY compressée (Z reconstruit) |
R16Float |
16 | half / float |
Shadow maps, AO |
R32Float |
32 | float |
Shadow maps haute précision, compute |
R32Uint |
32 | uint |
Visibility buffer, picking, index atomique |
Depth16Unorm |
16 | float |
Depth mobile — iOS, tile-based |
Depth32Float |
32 | float |
Depth standard macOS / Apple Silicon |
Depth32Float_Stencil8 |
64 | float + uint8 |
Depth + stencil combinés |
Stencil8 |
8 | uint8 |
Stencil seul (rare) |
HDR & EDR — RGBA16Float et Display P3
L'Extended Dynamic Range (EDR) d'Apple permet d'afficher des valeurs supérieures à 1.0 sur les écrans compatibles (ProMotion, XDR). Le pixel format du render target doit être capable de les contenir.
// Configuration MTKView pour EDR — déjà fait dans AppViewController.mm
_mtkView.colorPixelFormat = MTLPixelFormatRGBA16Float;
_mtkView.colorspace = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearDisplayP3);
RGBA16Float stocke chaque canal sur 16 bits flottants (half or float) — plage théorique ±65504. La valeur 1.0 correspond au blanc SDR standard ; les valeurs supérieures alimentent les pixels EDR sur écran XDR.
À titre d'exemple de représentation des plages de couleurs selon l'écran :
P3 c'est ton écran Mac, sRGB équivaut à une télévision 1080p, Rec.2020 est manipulable sur Blender mais il te faut un écran capable de l'afficher, sans quoi les couleurs seront converties en P3.
On retrouve donc une multitude de couleurs supplémentaires entre le rouge, le bleu, le noir, le blanc & le vert suite à la précision (16Float) et une plage étendue comparé à un écran ordinaire.
// MSL — émettre une valeur HDR depuis le fragment shader
fragment half4 fragment_hdr(VertexOut in [[stage_in]])
{
half4 color = half4(1.8h, 1.2h, 0.9h, 1.0h); // rouge HDR — dépasse 1.0
return color;
}
// Note : le type de retour est half4 (pas float4) — plus efficace pour RGBA16Float
RG11B10Float est une alternative intéressante pour les render targets HDR sans alpha — même coût mémoire que RGBA8Unorm (32 bits/texel) mais en flottant. Idéal pour un framebuffer intermédiaire de lumière avant la passe de tonemapping.
// 11 bits pour R et G, 10 bits pour B — pas d'alpha
MTL::PixelFormatRG11B10Float // 32 bpp, HDR, tile-friendly
Stencil — masquage par région
Le stencil buffer est un tableau d'entiers 8 bits (valeurs 0-255) associé pixel par pixel. Il permet de contraindre le rendu à certaines zones — sans coût de géométrie supplémentaire.
MTL::DepthStencilDescriptor* dsd = MTL::DepthStencilDescriptor::alloc()->init();
dsd->setDepthCompareFunction(MTL::CompareFunctionLess);
dsd->setDepthWriteEnabled(true);
// Écriture stencil — marquer les pixels d'un objet
MTL::StencilDescriptor* stencilDesc = MTL::StencilDescriptor::alloc()->init();
stencilDesc->setStencilCompareFunction(MTL::CompareFunctionAlways);
stencilDesc->setStencilFailureOperation(MTL::StencilOperationKeep);
stencilDesc->setDepthFailureOperation(MTL::StencilOperationKeep);
stencilDesc->setDepthStencilPassOperation(MTL::StencilOperationReplace); // écriture
stencilDesc->setWriteMask(0xFF);
stencilDesc->setReadMask(0xFF);
dsd->setFrontFaceStencil(stencilDesc);
dsd->setBackFaceStencil(stencilDesc);
stencilDesc->release();
// Valeur de référence — fournie par encoder, pas par descriptor
enc->setStencilReferenceValue(1);
Cas d'usage du stencil ;
- Outline / selection highlight
→ passe 1 : dessine l'objet, écrit 1 dans le stencil. Passe 2 : dessine l'objet légèrement agrandi, n'écrit que là où stencil != 1 → contour parfait sans lecture de profondeur complexe. - Portal / mirror
→ découpe une région de rendu dans laquelle une scène alternative est dessinée — utilisé pour les portails de type Mirror's Edge ou les reflets planaires. - Decals
→ projection de détails (impacts, taches) uniquement sur les surfaces marquées, sans recalculer la scène complète. - Shadow volumes
→ technique classique (Doom 3) : le stencil compte les entrées/sorties dans le volume d'ombre, puis masque les zones en ombre.
Formats compressés — ASTC
Les formats compressés ne sont pas accessibles en render target — uniquement en lecture (ShaderRead). Ils sont encodés hors ligne (Xcode Asset Catalog, ou Metal Performance Shaders via une compute pass) et restent compressés en VRAM. Le GPU les décompresse à la volée lors du sampling, sans coût CPU.
// Formats ASTC disponibles sur Apple Silicon
MTL::PixelFormatASTC_4x4_LDR // 8 bpp — qualité maximale
MTL::PixelFormatASTC_6x6_LDR // 3.56 bpp — bon compromis
MTL::PixelFormatASTC_8x8_LDR // 2 bpp — compression forte
MTL::PixelFormatASTC_4x4_sRGB // variante sRGB pour albedo
MTL::PixelFormatASTC_4x4_HDR // Apple Silicon uniquement — HDR compressé
| Format ASTC | Bits/texel | Ratio vs RGBA8 | Usage |
|---|---|---|---|
ASTC_4x4 |
8 bpp | ÷ 4 | Albedo, textures complexes |
ASTC_6x6 |
3.56 bpp | ÷ 9 | Normal maps, terrain |
ASTC_8x8 |
2 bpp | ÷ 16 | Roughness/AO, grandes surfaces |
ASTC_4x4_HDR |
8 bpp | ÷ 8 vs RGBA16F | Skybox HDR, light maps |
Sur iOS et iPadOS (toute la gamme Apple Silicon), ASTC est le format de compression natif. Il remplace avantageusement un chargement PNG/JPEG en texture — la décompression CPU est évitée, la VRAM consommée est 4 à 16 fois inférieure, et les performances de sampling sont identiques à un format non compressé.
Xcode Asset Catalog encode automatiquement en ASTC si la cible inclut iOS. En dehors de Xcode, on peut appeler MTKTextureLoader avec l'option generateMipmaps ou encoder via Metal Performance Shaders en compute.
Guide des décisions
Color render target SDR → RGBA8Unorm_sRGB (+ sRGB colorspace)
Color render target HDR / EDR → RGBA16Float (+ ExtendedLinearDisplayP3)
HDR sans alpha, bande passante → RG11B10Float
Wide color sortie Display P3 → RGB10A2Unorm
Albedo texture → RGBA8Unorm_sRGB ou ASTC_4x4_sRGB
Normal map (XY) → RG8Unorm ou ASTC_6x6_LDR
Roughness / Metallic → R8Unorm ou ASTC_8x8_LDR
Skybox HDR → RGBA16Float ou ASTC_4x4_HDR
Depth (macOS / desktop) → Depth32Float + StorageModePrivate
Depth (iOS / Apple Silicon) → Depth32Float + StorageModeMemoryless
Depth + Stencil → Depth32Float_Stencil8
Compute intermédiaire (lecture) → R32Float / RGBA32Float
Visibility / picking → R32Uint + atomic_fetch_or
Les formats compressés ASTC restent compressés en VRAM — la décompression est faite à la volée par le hardware, sans intervention CPU.