# BVLE Voxels — Troubleshooting & Pièges techniques ## Table des matières - [APIs Wicked utilisées](#apis-wicked-utilisées) - [Coordonnées logiques vs physiques](#coordonnées-logiques-vs-physiques--piège-majeur) - [Triplanar UDN Normal Mapping](#triplanar-udn-normal-mapping--pièges-majeurs) - [Shaders custom — Pièges importants](#shaders-custom--pièges-importants) 1. [Root signature obligatoire](#1-root-signature-obligatoire) 2. [Root signature Wicked (HLSL 6.6+)](#2-root-signature-wicked-hlsl-66) 3. [Chemins des shaders](#3-chemins-des-shaders) 4. [dxcompiler.dll manquant](#4-dxcompilerdll-manquant) 5. [CreateBuffer prend void*](#5-createbuffer-prend-void) 6. [Winding des triangles](#6-winding-des-triangles--piège-majeur) 7. [DrawInstancedIndirectCount stride 20 bytes](#7-drawinstancedindirectcount--piège-majeur) 8. [SV_VertexID et startVertexLocation](#8-sv_vertexid-et-startvertexlocation--piège-majeur) 9. [Barriers sur buffers indirect](#9-barriers-sur-buffers-indirect) 10. [PushConstants après BindComputeShader](#10-pushconstants-après-bindcomputeshader--piège-majeur) - [CreateBuffer avec capacity > data size](#createbuffer-avec-capacity--data-size) - [BLAS/TLAS per-frame recreation — VRAM leak](#blastlas-per-frame-recreation--vram-leak) - [Diagnostics et debugging](#diagnostics-et-debugging) - [Smooth Surface Nets — Rendu facetté et jointure blocky](#smooth-surface-nets--rendu-facetté-et-jointure-blocky) - [Gestion des resource states DX12 (buffers)](#gestion-des-resource-states-dx12-buffers) --- ## APIs Wicked utilisées | Besoin | API Wicked | |--------|-----------| | Clavier WASD | `wi::input::Down(CHARACTER_RANGE_START + offset)` (pas de `KEYBOARD_BUTTON_W`) | | Souris delta | `wi::input::GetMouseState().delta_position` | | Cacher curseur | `wi::input::HidePointer(bool)` | | Shader loading | `wi::renderer::LoadShader()` - compile auto les .hlsl en .cso si absent | | PSO states | `wi::renderer::GetRasterizerState()` etc. retournent des pointeurs (pas besoin de `&`) | | Render pass | `RenderPassImage::RenderTarget(texture, loadOp, storeOp, layoutBefore, layoutAfter, subresource=-1)` | | Font overlay | `wi::font::Params` est un struct - setter les membres un par un | | Camera | `CameraComponent::At` est une **direction** (utilisé avec `XMMatrixLookToLH`), pas un point cible | | Buffer create | `device->CreateBuffer(desc, raw_data_ptr, buffer)` — PAS de `SubresourceData` pour les buffers ! | | Texture create | `device->CreateTexture(desc, subresourceData_ptr, texture)` — utilise `SubresourceData*` (différent de CreateBuffer) | | Buffer update | `device->UpdateBuffer(buffer, data, cmd, size, offset)` | | Push constants | `device->PushConstants(data, size, cmd)` — mappés à `register(b999)`, taille fixe 48 bytes (12 × uint32) | | Command list | `device->BeginCommandList()` — nouveau cmd list pour render passes séparés | | Render pass | NE JAMAIS imbriquer ! Un seul render pass actif par command list | | Debug DX12 | Passer `"debugdevice"` en argument pour activer la couche de debug D3D12 | | Logging | `wi::backlog::post(message, logLevel)` — préférer au logging fichier | | Screen size (draw) | **`GetLogicalWidth()`/`GetLogicalHeight()`** pour `wi::font` et `wi::image` (PAS `GetPhysicalWidth`) | | Solid rect draw | `wi::image::Draw(wi::texturehelper::getWhite(), params, cmd)` — ne PAS passer `nullptr` | --- ## Coordonnées logiques vs physiques — Piège majeur Wicked Engine distingue deux systèmes de coordonnées écran : - **Physical** (`GetPhysicalWidth()`/`GetPhysicalHeight()`) : pixels réels du backbuffer. Utilisé pour créer les render targets, viewports, et textures GPU. - **Logical** (`GetLogicalWidth()`/`GetLogicalHeight()`) : pixels DPI-scaled. **Tout le système 2D de Wicked** (`wi::font::Draw`, `wi::image::Draw`, `wi::image::Params::pos/siz`) travaille en coordonnées logiques. **Symptôme** : éléments HUD décalés, crosshair excentré, texte hors écran. ```cpp // ❌ FAUX — décalé si DPI scaling ≠ 100% float cx = (float)GetPhysicalWidth() * 0.5f; wi::font::Params fp; fp.posX = cx; // ✅ CORRECT float cx = GetLogicalWidth() * 0.5f; wi::font::Params fp; fp.posX = cx; ``` **Pour dessiner un rectangle solide** (pas de texture) : ```cpp // ❌ FAUX — ne dessine rien wi::image::Draw(nullptr, params, cmd); // ✅ CORRECT — utiliser la texture blanche 1x1 intégrée #include "wiTextureHelper.h" wi::image::Draw(wi::texturehelper::getWhite(), params, cmd); ``` La projection 2D est définie dans `wiCanvas.h` : ```cpp GetProjection() = XMMatrixOrthographicOffCenterLH(0, GetLogicalWidth(), GetLogicalHeight(), 0, -1, 1); ``` --- ## Triplanar UDN Normal Mapping — Pièges majeurs L'implémentation UDN (Unreal Derivative Normal) triplanar pour les normal maps a trois subtilités critiques : ### 1. NE PAS utiliser `abs(normal)` dans la formule UDN La référence Ben Golus utilise `abs(normal)` car elle cible des terrains (normales toujours vers le haut). Pour des voxels avec 6 directions de faces, `abs()` force la composante dominante à être positive, **inversant l'éclairage sur les faces -X, -Y et -Z**. ```hlsl // ❌ FAUX — inverse les normales sur 3 faces (le NdotL est faux) float3 absN = abs(normal); float3 worldNX = float3(tnX.xy + absN.zy, absN.x).zyx; // Face -X: absN.x = 1 → résultat pointe vers +X au lieu de -X // ✅ CORRECT — utiliser le normal brut float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx; // Face -X: normal.x = -1 → résultat pointe bien vers -X ``` **Diagnostic** : ombres RT correctes (elles utilisent la géométrie) mais éclairage direct inversé sur certaines faces → contradiction visuelle. ### 2. Correction de signe pour les faces négatives Les UV sont miroir sur les faces négatives. Le `sign(normal)` corrige la composante tangent-space X : ```hlsl float3 axisSign = sign(normal); tnX.x *= axisSign.x; // Flip U-tangent pour -X tnY.x *= axisSign.y; // Flip U-tangent pour -Y tnZ.x *= axisSign.z; // Flip U-tangent pour -Z ``` ### 3. Flip green channel pour les normal maps OpenGL (seulement projection Y) Les textures `normal_gl` ont le green channel inversé par rapport à DX. En triplanar, seule la **projection Y** (faces horizontales, UV=xz) nécessite le flip — les projections X et Z ont V=world Y qui est naturellement correct. ```hlsl // ❌ FAUX — casse les faces verticales tnX.y = -tnX.y; tnY.y = -tnY.y; tnZ.y = -tnZ.y; // ✅ CORRECT — seulement la projection Y tnY.y = -tnY.y; ``` **Formule complète correcte** : ```hlsl float3 axisSign = sign(normal); float3 tnX = sample(wp.zy).rgb * 2.0 - 1.0; float3 tnY = sample(wp.xz).rgb * 2.0 - 1.0; float3 tnZ = sample(wp.xy).rgb * 2.0 - 1.0; tnY.y = -tnY.y; // GL flip Y-projection only tnX.x *= axisSign.x; // sign correction tnY.x *= axisSign.y; tnZ.x *= axisSign.z; float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx; // RAW normal float3 worldNY = float3(tnY.xy + normal.xz, normal.y).xzy; float3 worldNZ = float3(tnZ.xy + normal.xy, normal.z); return normalize(worldNX * w.x + worldNY * w.y + worldNZ * w.z); ``` --- ## Shaders custom — Pièges importants Les shaders custom doivent respecter le **binding model de Wicked Engine** : ### 1. Root signature obligatoire Chaque shader DOIT avoir une root signature DX12 intégrée, soit via `#include "globals.hlsli"` (auto), soit via `[RootSignature(MACRO)]` sur le entry point. ### 2. Root signature Wicked (HLSL 6.6+) - `b999` → push constants (12 × uint32 = 48 bytes max) - `b0, b1, b2` → CBV root descriptors - `t0-t15, u0-u15` → dans une descriptor table partagée - `s0-s7` → samplers dynamiques - `s100-s109` → static samplers (linear, point, aniso, etc.) ### 3. Chemins des shaders - `SHADERPATH` = `/shaders/hlsl6/` — où les `.cso` compilés sont stockés - `SHADERSOURCEPATH` = `../../engine/WickedEngine/shaders/` — où les `.hlsl` sources sont cherchés - Les shaders custom doivent être copiés dans `SHADERSOURCEPATH` (sous-dossier `voxel/`) - `LoadShader(stage, shader, "voxel/voxelVS.cso")` → compile `SHADERSOURCEPATH/voxel/voxelVS.hlsl` si `.cso` absent ### 4. dxcompiler.dll manquant `dxcompiler.dll` doit être à côté de l'exe sinon la compilation runtime échoue silencieusement. ### 5. CreateBuffer prend void* `CreateBuffer` prend `void*`, pas `SubresourceData*`. L'API texture (`CreateTexture`) prend bien `SubresourceData*`. ### 6. Winding des triangles — PIÈGE MAJEUR Wicked Engine utilise `front_counter_clockwise = true` + `CullMode::BACK` (state `RSTYPE_FRONT`). Malgré cela, les quads voxel doivent utiliser un winding **CW** (clockwise) comme défaut, pas CCW. Confirmé empiriquement via `SV_IsFrontFace` : avec des corners CCW standard, DX12 voit tous les triangles comme **back-facing**. La règle pour nos tangent axes U/V : - `cross(U,V) = N` (faces +X, -Y, +Z) → corners **CW** pour être front-facing - `cross(U,V) ≠ N` (faces -X, +Y, -Z) → corners **CCW** pour être front-facing ``` CW corners: (0,0)(0,1)(1,0), (1,0)(0,1)(1,1) ← défaut CCW corners: (0,0)(1,0)(0,1), (0,1)(1,0)(1,1) ← faces 1,2,5 ``` ### 7. DrawInstancedIndirectCount — PIÈGE MAJEUR Les command signatures de Wicked Engine pour `*IndirectCount` incluent un **push constant** (1 × uint32, écrit dans `b999[0]`) AVANT chaque `D3D12_DRAW_ARGUMENTS`. Le stride par draw entry est donc **20 bytes**, pas 16. Layout mémoire du buffer d'args indirect : ``` [uint32 pushConstant][uint32 vertexCount][uint32 instanceCount][uint32 startVertex][uint32 startInstance] 4 bytes 16 bytes (D3D12_DRAW_ARGUMENTS) = 20 bytes par draw entry ``` Le push constant est écrit automatiquement par `ExecuteIndirect` dans `b999[0]` (premier champ de la struct push constants, soit `chunkIndex` dans notre cas). Les autres champs de b999 (quadOffset, flags...) restent tels que définis par le `PushConstants()` appelé avant `DrawInstancedIndirectCount`. **En mode MDI, le push constant est utilisé pour packer `chunkIndex | (faceIndex << 16)`**. Le VS décode ces deux valeurs et reconstruit le quadOffset depuis le `GPUChunkInfo` : ```hlsl chunkIndex = push.chunkIndex & 0xFFFF; faceIdx = push.chunkIndex >> 16; quadIndex = chunkInfo[chunkIndex].quadOffset + faceOffset[faceIdx] + (vertexID / 6); ``` **Source** : `wiGraphicsDevice_DX12.cpp` lignes 3930-3939 — la command signature est créée par PSO avec `D3D12_INDIRECT_ARGUMENT_TYPE_CONSTANT` + `D3D12_INDIRECT_ARGUMENT_TYPE_DRAW`. ### 8. SV_VertexID et startVertexLocation — PIÈGE MAJEUR Avec `ExecuteIndirect` (DrawInstancedIndirectCount), `SV_VertexID` **n'inclut PAS de manière fiable** `startVertexLocation` de `D3D12_DRAW_ARGUMENTS`. Observé sur AMD RDNA 4 (RX 9070 XT) : SV_VertexID commence toujours à 0 pour chaque draw, ignorant startVertexLocation. **Solution** : toujours mettre `startVertexLocation = 0` dans les indirect args, et passer l'offset des quads par un autre canal (push constant + GPUChunkInfo lookup). Ne JAMAIS compter sur `startVertexLocation` pour encoder un offset dans le mega-buffer. ### 9. Barriers sur buffers indirect Les buffers `Usage::DEFAULT` démarrent en COMMON et décayent vers COMMON après chaque exécution de command list. La promotion implicite COMMON → COPY_DST (via UpdateBuffer) et COMMON → INDIRECT_ARGUMENT (via DrawInstancedIndirectCount) fonctionne sans barriers explicites. C'est le même pattern que les SRV buffers (megaQuadBuffer_, chunkInfoBuffer_) qui passent de COPY_DST à SRV usage sans barrier en Phase 2.1. **Pour la Phase 2.3 (compute cull)**, des barriers explicites SONT nécessaires : - `drawCountBuffer_` : COPY_DST → UAV (après UpdateBuffer zero) puis UAV → INDIRECT_ARGUMENT (après dispatch) - `indirectArgsBuffer_` : UNDEFINED → UAV (COMMON après decay, `ResourceState::UNDEFINED = 0` = COMMON en Wicked) puis UAV → INDIRECT_ARGUMENT - Wicked Engine appelle `DiscardResource()` quand `state_before == UNDEFINED`, ce qui est OK (le compute écrase les données) ### 10. PushConstants après BindComputeShader — PIÈGE MAJEUR `PushConstants()` dispatche vers `SetGraphicsRoot32BitConstants` ou `SetComputeRoot32BitConstants` selon l'état actif : - Si `active_pso != nullptr` → **GRAPHICS** push constants - Sinon si `active_cs != nullptr` → **COMPUTE** push constants Après `BindComputeShader` + `Dispatch`, `active_cs` reste actif. Appeler `PushConstants` à ce moment écrit dans les push constants **compute**, pas **graphics**. Le vertex shader ne voit jamais la valeur ! **Règle** : toujours appeler `PushConstants` **APRÈS** `BindPipelineState` (qui set `active_pso`) pour cibler les push constants graphics. L'ordre correct : ```cpp BindPipelineState(&pso_); // ← active_pso = &pso_ PushConstants(&data, ...); // ← SetGraphicsRoot32BitConstants ✓ Draw*(...); ``` --- ## CreateBuffer avec capacity > data size **Symptôme** : crash dans `memmove` au démarrage (EXCEPTION_ACCESS_VIOLATION, reading past vector bounds). **Cause racine** : `CreateBuffer(desc, data_ptr, buffer)` dans Wicked Engine copie `desc.size` bytes depuis `data_ptr` via `memmove`. Quand le buffer est pré-alloué avec une capacité plus grande que les données réelles (ex: 25% de headroom), `desc.size > vector.size() * stride` → lecture hors limites. **Solution** : créer le buffer SANS données initiales (`CreateBuffer(desc, nullptr, buffer)`), puis uploader les données dans `Render()` via `UpdateBuffer(buffer, data, cmd, actual_size)` qui prend une taille explicite. **Fichiers** : `VoxelRenderer.cpp` — `uploadTopingData()`, `uploadSmoothData()`, `uploadSmoothDataFast()` --- ## BLAS/TLAS per-frame recreation — VRAM leak **Symptôme** : VRAM explose dès le début de l'animation F3 (terrain dynamique). Le GPU alloue des centaines de MB par seconde. **Cause racine** : `CreateRaytracingAccelerationStructure` alloue de la mémoire GPU (scratch + result buffers) pour chaque BLAS/TLAS. Pendant l'animation, le vertex count change à chaque frame → le BLAS est recréé chaque frame. L'ancienne allocation est libérée en différé (3 frames de latence GPU) → accumulation VRAM. Particulièrement grave pour la toping BLAS (~23M vertices, ~7.7M triangles) — chaque recréation alloue des dizaines de MB. **Solution — allocation capacity-based** : 1. **BLAS** : Créer avec `vertex_count = capacity` (25% headroom). Avant chaque `BuildRaytracingAccelerationStructure`, mettre à jour `desc.bottom_level.geometries[0].triangles.vertex_count` avec le count réel. Le Build reconstruit le BVH in-place sans réallouer. Ne recréer le BLAS que quand le count dépasse la capacité. ```cpp // Création (une seule fois, ou quand capacité dépassée) if (!blas.IsValid() || vertCount > blasCapacity) { blasCapacity = vertCount + vertCount / 4; // 25% headroom // ... desc avec vertex_count = blasCapacity ... dev->CreateRaytracingAccelerationStructure(&desc, &blas); } // Build chaque frame (update desc.vertex_count avec le count réel) blas.desc.bottom_level.geometries[0].triangles.vertex_count = vertCount; blas.desc.bottom_level.geometries[0].triangles.index_count = vertCount; dev->BuildRaytracingAccelerationStructure(&blas, cmd, nullptr); ``` 2. **TLAS** : Ne recréer que quand le nombre d'instances change (ex: 2→3 quand la toping BLAS devient valide). Sinon, `BuildRaytracingAccelerationStructure` suffit car les pointeurs BLAS restent stables (les BLAS ne sont pas recréés). 3. **Deferred upload pattern** : Les positions BLAS toping sont uploadées via `UpdateBuffer` dans `Render()` (flag `topingBLASDirty_`) avant le BLAS rebuild, car `Update()` n'a pas de CommandList. **Fichiers** : `VoxelRenderer.cpp` — `buildAccelerationStructures()`, `VoxelRenderPath::Render()` --- ## Diagnostics et debugging **Crash handler SEH** (`main.cpp`) : `SetUnhandledExceptionFilter` écrit : - `bvle_crash.log` : stack trace avec symboles + adresses - `bvle_crash.dmp` : minidump analysable avec Visual Studio - Nécessite `dbghelp.lib` et build avec symbols (`RelWithDebInfo` ou `Debug`) **D3D12 Debug Layer** : lancer avec `BVLEVoxels.exe debugdevice` pour activer. Active aussi DRED (Device Removed Extended Data) pour diagnostiquer les GPU hangs. **Erreurs GPU courantes** : - `DXGI_ERROR_INVALID_CALL` → render pass imbriqué ou resource state invalide - `DXGI_ERROR_DEVICE_HUNG` → shader en boucle infinie ou accès mémoire hors limites - Dialog bloquant avec `messageBox` → vient de `wi::helper::messageBox()`, ne pas confondre avec un crash **Détection de crash GPU depuis CLI (Claude Code)** : les crashs GPU (`DXGI_ERROR_INVALID_CALL`, device removed) affichent une **modale Windows bloquante** via `wi::helper::messageBox()`. `timeout` tue le process sans détecter le crash. Pour détecter correctement : 1. **NE PAS utiliser `timeout`** pour tester — demander à l'utilisateur de lancer manuellement 2. Vérifier `bvle_backlog.txt` après exécution (contient les erreurs DX12) 3. Vérifier `bvle_crash.log` et `bvle_crash.dmp` pour les crashs SEH 4. Lancer avec `debugdevice` pour obtenir les messages de validation D3D12 détaillés dans le backlog 5. Un exit code non-zéro n'est PAS fiable : `timeout` renvoie 124, la modale attend indéfiniment **Backlog Wicked** : `wi::backlog::SetLogFile("bvle_backlog.txt")` redirige les logs vers un fichier. Touche `~` (tilde) pour toggler la console à l'écran. **Mode debug face-color** : lancer avec `BVLEVoxels.exe debug` pour activer. Génère un monde de test (blocs isolés) et colore chaque face selon sa direction : - Bright Red / Dark Red = +X / -X - Bright Green / Dark Green = +Y / -Y - Bright Blue / Dark Blue = +Z / -Z --- ## Smooth Surface Nets — Rendu facetté et jointure blocky ### Problème 1 : Rendu smooth facetté malgré normales lisses **Symptôme** : en mode debug (FLAT, NdotL, NORMAL), la surface smooth est parfaitement lisse. Mais en rendu final (ALL), elle apparaît facettée avec des arêtes de triangles visibles. **Cause racine** : `geoN` (geometric normal via `ddx(worldPos)`/`ddy(worldPos)`) était utilisé pour le triplanar sampling (poids de projection) ET le normal mapping. Cette valeur est la **face normal du triangle à l'écran** — elle change de manière **discontinue** à chaque arête de triangle. Résultat : 1. **Poids triplanar discontinus** → la texture saute aux arêtes (coutures visibles) 2. **Normal map discontinu** → la perturbation normale diffère par triangle → NdotL facetté Les modes debug étaient lisses car ils utilisaient `flatN` (smooth normal **avant** perturbation normal map), pas le `N` perturbé. **Correction** : utiliser `N` (smooth interpolated normal) pour **tout** le triplanar dans `voxelSmoothPS.hlsl` : - Poids triplanar albedo/heightmap → `N` (pas `geoN`) - Normal map sampling → `N` (pas `geoN`) - `geoN` n'est plus calculé/utilisé du tout `N` varie continûment entre vertices → transitions lisses partout. ### Problème 2 : Jointure visible smooth/blocky **Symptôme** : contraste visible entre faces smooth et blocky adjacentes, quasi-coplanaires. **Causes racines** (cumulatives) : 1. **Traitements per-material dans un seul PS** — le blocky PS avait un shading spécifique grass (side darkening 60%, warm shift chromatique, ambient boost ×1.15) absent du smooth PS. Pour une face grass +X, ça créait ~40% d'écart de luminosité. 2. **Smooth normals biaisées aux frontières** — les vertex normals aux arêtes 90° (mur smooth → sol) étaient moyennées entre faces perpendiculaires (consistency ≈ 0.707), produisant une normale biaisée vers +Y au lieu de +X pur. **Correction** : - **Supprimer les traitements per-material hardcodés** des deux PS. Quand on aura besoin de shading par matériau, le rendre data-driven et l'appliquer identiquement dans les deux shaders. - **Consistency-based vertex normal blend** dans `voxelSmoothCS.hlsl` : métrique `|Σfn| / Σ|fn|` qui mesure l'accord des face normals incidentes. Les vertices à faible consistency (arêtes nettes, frontières) reçoivent la face normal pure ; les vertices à haute consistency (surfaces courbes) gardent la smooth normal. ### Calibration du seuil de consistency Le seuil `smoothstep(low, high, consistency)` contrôle le compromis lisse/net : | Seuil | con=0.707 (90° edge) | con=0.85 (courbe) | con=0.95 (pente) | Résultat | |---|---|---|---|---| | `(0.85, 1.0)` | t=0 face ✓ | t=0 face ✗ | t=0.26 ≈ face ✗ | Trop agressif, tout facetté | | `(0.60, 0.85)` | t=0.27 ≈ 73% face | t=1.0 smooth ✓ | t=1.0 smooth ✓ | Frontière visible, intérieur lisse | | `(0.70, 0.90)` | t≈0 face ✓ | t=0.84 smooth ✓ | t=1.0 smooth ✓ | **Bon compromis** | **Valeur retenue : `smoothstep(0.70, 0.90)`** — les arêtes 90° (con ≤ 0.707) reçoivent 100% face normal (jointure nette avec blocky), les courbes modérées (con > 0.85) restent smooth. ### Normal map strength Le smooth PS utilise `nmStrength * 0.7` (vs `nmStrength * 1.0` pour blocky). Les surfaces courbes nécessitent des normal maps atténuées pour que les perturbations ne cassent pas la continuité visuelle du smooth shading. ### Règles - **Toute modification de lighting/texturing** dans `voxelPS.hlsl` doit être portée dans `voxelSmoothPS.hlsl` (et vice-versa) - **Ne JAMAIS utiliser `geoN`** (ddx/ddy) dans le smooth PS pour le triplanar ou le normal mapping — utiliser `N` exclusivement - Les deux PS doivent produire un résultat identique sur des faces coplanaires de même matériau **Fichiers** : `shaders/voxelSmoothCS.hlsl` (consistency blend), `shaders/voxelSmoothPS.hlsl` (triplanar + normal map), `shaders/voxelPS.hlsl` (blocky reference) --- ## Gestion des resource states DX12 (buffers) **Wicked Engine ne fait AUCUN tracking automatique d'état pour les buffers.** Les `GPUBarrier::Buffer(buf, before, after)` sont passées directement à D3D12 sans validation. **Le `state_before` DOIT correspondre à l'état DX12 réel, sinon → DXGI_ERROR_INVALID_CALL.** **Pièges critiques :** - `UpdateBuffer()` → appelle `CopyBufferRegion` sans aucune barrier. Le buffer **DOIT** être en COPY_DST (ou COMMON pour promotion implicite sur frame 1). - Après `DrawInstancedIndirectCount`, les buffers indirect restent en **INDIRECT_ARGUMENT**. Appeler `UpdateBuffer` dessus au frame suivant → crash car pas de transition INDIRECT_ARGUMENT → COPY_DST. - Les buffers créés avec `Usage::DEFAULT` démarrent en état **COMMON** (D3D12). COMMON supporte la promotion implicite vers COPY_DST, SRV, etc. mais **PAS vers UAV**. - Solution recommandée : **tracker l'état manuellement** avec un `mutable ResourceState` et faire des barriers explicites entre chaque usage.