bvle-voxels/TROUBLESHOOTING.md
Samuel Bouchet afb86446cd Fix VRAM leak: capacity-based BLAS/TLAS + deferred toping BLAS upload
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.
2026-03-30 21:37:39 +02:00

228 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.