Per-frame CreateRaytracingAccelerationStructure calls during F3 animation caused VRAM explosion (especially toping BLAS at ~23M vertices). Now all 3 BLASes use capacity-based allocation with 25% headroom — only recreated when vertex count exceeds capacity, otherwise just BuildRaytracingAS with updated desc.vertex_count. TLAS only recreated when instance count changes. Also adds deferred toping BLAS position upload via UpdateBuffer in Render() (topingBLASDirty_ flag), enabling toping shadows to update during animation. Split CLAUDE.md into CLAUDE.md + TROUBLESHOOTING.md for maintainability.
228 lines
14 KiB
Markdown
228 lines
14 KiB
Markdown
# BVLE Voxels — Troubleshooting & Pièges techniques
|
||
|
||
## Table des matières
|
||
|
||
- [APIs Wicked utilisées](#apis-wicked-utilisées)
|
||
- [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)
|
||
- [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 |
|
||
|
||
---
|
||
|
||
## 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` = `<exe_dir>/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
|
||
|
||
---
|
||
|
||
## 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.
|