bvle-voxels/CLAUDE.md
2026-03-25 19:38:50 +01:00

295 lines
15 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 - Prototype de Moteur Voxel Hybride
## Vue d'ensemble
Prototype de moteur voxel basé sur **Wicked Engine** (MIT, C++17, DX12/Vulkan) pour valider les performances de rendu sur GPU moderne (AMD RDNA 2+ / Nvidia RTX 3060+). Le document de spécification complet est dans `voxel_engine_spec.docx` à la racine du projet.
Cible : 60+ fps en 1440p, monde de 512x512x256 voxels visibles.
## Architecture
```
bvle-voxels/
├── CMakeLists.txt # Build CMake racine
├── engine/ # Wicked Engine (clone --depth 1, branche main)
│ └── WickedEngine/shaders/voxel/ # Nos shaders copiés ici pour compilation DXC
├── src/
│ ├── voxel/ # Bibliothèque VoxelEngine (static lib)
│ │ ├── VoxelTypes.h # Types fondamentaux (VoxelData, PackedQuad, MaterialDesc, ChunkPos)
│ │ ├── VoxelWorld.h/.cpp # Monde voxel (hashmap de chunks, génération procédurale)
│ │ ├── VoxelMesher.h/.cpp # Binary Greedy Mesher CPU
│ │ └── VoxelRenderer.h/.cpp# Renderer + VoxelRenderPath (sous-classe RenderPath3D)
│ └── app/
│ └── main.cpp # Point d'entrée Win32 + crash handler SEH
├── shaders/ # Sources HLSL des shaders voxel (copiés dans engine/ au build)
│ ├── voxelCommon.hlsli # Root signature et CB partagés (inclus par VS et PS)
│ ├── voxelVS.hlsl # Vertex shader (vertex pulling)
│ └── voxelPS.hlsl # Pixel shader (triplanar + lighting)
└── CLAUDE.md
```
## Build
### Prérequis
- CMake 3.19+ (`winget install Kitware.CMake`)
- Visual Studio 2022 Build Tools (`winget install Microsoft.VisualStudio.2022.BuildTools`)
- Windows SDK 10.0.26100+ (`winget install Microsoft.WindowsSDK.10.0.26100`)
### Commandes
```bash
# Configurer (depuis la racine du projet)
cmake -B build -G "Visual Studio 17 2022" -A x64 -DCMAKE_SYSTEM_VERSION=10.0.26100.0
# Compiler
cmake --build build --config Release --target BVLEVoxels --parallel
# Exécutable produit dans build/Release/BVLEVoxels.exe
```
Le SDK 10.0.26100 est requis car les headers DX12 (`d3dx12_check_feature_support.h`) fournis par Wicked Engine ne sont pas compatibles avec le SDK 22621.
### Post-build automatique (CMakeLists.txt)
Le build copie automatiquement :
1. `dxcompiler.dll` → à côté de l'exe (requis pour la compilation runtime des shaders)
2. `shaders/*.hlsl``engine/WickedEngine/shaders/voxel/` (pour que `LoadShader` les trouve via `SHADERSOURCEPATH`)
3. `engine/Content/` → à côté de l'exe (assets Wicked Engine)
## Intégration Wicked Engine
### Backend graphique
Wicked Engine utilise **DX12 par défaut sur Windows**, Vulkan sur Linux. Les shaders sont écrits en **HLSL** et compilés via DXC vers :
- `shaders/hlsl6/*.cso` pour DX12
- `shaders/spirv/*.spv` pour Vulkan
Pour forcer Vulkan sur Windows, passer `"vulkan"` en argument de ligne de commande.
### Point d'entrée et architecture de rendu
`VoxelRenderPath` hérite de `wi::RenderPath3D`. **IMPORTANT** : le rendu voxel utilise ses propres render targets (`voxelRT_`, `voxelDepth_`) et est exécuté dans `Render()` sur un **command list dédié** (`device->BeginCommandList()`). Le résultat est ensuite composité dans `Compose()` via `wi::image::Draw()`.
**NE JAMAIS créer un render pass dans `Compose()`** : cette méthode est appelée à l'intérieur du render pass du swapchain. Imbriquer des render passes est interdit en D3D12 (cause `DXGI_ERROR_INVALID_CALL → device removed`).
Architecture correcte :
```
Render() → RenderPath3D::Render() // Wicked rend sa scène
→ device->BeginCommandList() // Nouveau cmd list
→ renderer.render(cmd, ...) // Notre render pass (clear + draw voxels → voxelRT_)
Compose() → RenderPath3D::Compose() // Wicked affiche son résultat
→ wi::image::Draw(voxelRT_) // On overlay nos voxels par-dessus
```
La caméra est gérée manuellement dans `Update()` en écrivant directement `camera->Eye`, `camera->At` (direction LookTo), `camera->Up`.
### 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` doit être à côté de l'exe** sinon la compilation runtime échoue silencieusement.
5. **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
```
### 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.
### 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.
**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
## Détails d'implémentation
### VoxelData (16 bits)
```
[15:8] material ID (256 matériaux)
[7:4] flags (smooth, transparent, emissive, custom)
[3:0] metadata (orientation, variant)
```
### PackedQuad (64 bits = 8 octets par quad)
```
[5:0] position X (0-63)
[11:6] position Y (0-63)
[17:12] position Z (0-63)
[23:18] width (1-32)
[29:24] height (1-32)
[32:30] face (0-5 : +X,-X,+Y,-Y,+Z,-Z)
[40:33] material ID
[48:41] AO (4x2 bits par coin)
[63:49] flags (réservés)
```
### Binary Greedy Mesher (CPU, `VoxelMesher.cpp`)
1. **Masques binaires** : pour chaque axe (X,Y,Z), `solid[u][v]` = bitmask 32 bits de voxels solides
2. **Face culling** : `visible = solid & ~(solid >> 1)` pour faces positives (shift adapté par direction), avec lookup cross-chunk aux frontières
3. **Greedy merge** : par tranche de profondeur, grille 2D de material IDs, expansion rectangulaire maximale (largeur puis hauteur)
### Génération procédurale (`VoxelWorld.cpp`)
- Perlin noise 3D (permutation-based, seed configurable)
- fBm 5 octaves pour le heightmap
- Caves : `|fbm(x,y,z)| < threshold` en 3D
- Matériaux par altitude : sable < 25, herbe 25-70, pierre 70-90, neige > 90
- Chunks générés en Y = 0..7 (hauteur max 256 blocs)
### Renderer (`VoxelRenderer.cpp`)
- **Vertex pulling** : pas de vertex buffer classique, le VS lit un `StructuredBuffer<PackedQuad>` via `SV_VertexID`
- **Pipeline** : PSO avec `RSTYPE_FRONT` (backface cull), `DSSTYPE_DEFAULT` (depth test), `BSTYPE_OPAQUE`
- **Per-chunk** : push constants (b999, 48 bytes) pour la position monde du chunk, bind du quad buffer en `t0`
- **Textures** : texture array 2D (256x256, 5 layers) générée procéduralement, triplanar mapping dans le PS
- **Culling** : distance-based simple (512 blocs), pas de frustum culling GPU
- **Render targets propres** : `voxelRT_` (R8G8B8A8) + `voxelDepth_` (D32_FLOAT), rendu dans `Render()` sur cmd list dédié
- **Composition** : overlay sur le swapchain via `wi::image::Draw()` dans `Compose()`
- **Stats overlay** : affichage HUD des chunks/quads/draw calls via `wi::font::Draw`
## Phases de développement (spec)
### Phase 1 - Setup et meshing de base [FAIT]
- Fork Wicked Engine, structure de modules
- VoxelWorld avec génération procédurale Perlin (rayon 4 chunks = ~150 chunks)
- Binary Greedy Mesher CPU (~300K quads pour le monde initial)
- Rendu basique avec vertex pulling et texture array
- Caméra libre de navigation (WASD + souris)
- Crash handler SEH avec stack trace symbolique
### Phase 2 - Performance GPU [A FAIRE]
- Porter le mesher en compute shader
- MultiDrawIndirect (un seul draw call pour tous les chunks)
- Frustum culling GPU + indirect args
- Backface culling par orientation (6 groupes de faces)
- Benchmark CPU vs GPU mesher
### Phase 3 - Texture blending [A FAIRE]
- Triplanar mapping (déjà en place, à affiner)
- Height-based blending aux frontières de matériaux
- Heightmaps dans le canal alpha ou texture séparée
- Neighbor material ID dans le vertex format (8 bits dans les flags réservés)
### Phase 4 - Toping [A FAIRE]
- TopingSystem avec bitmask d'adjacence 4 bits (16 variantes)
- Instance buffer GPU par chunk
- Instanced draw dans le G-buffer
- 2-3 types de test (rebord de pierre, bordure d'herbe)
### Phase 5 - Rendu smooth [A FAIRE]
- Surface Nets (ou Marching Cubes) en compute shader
- Flag `smooth` dans VoxelData
- Coexistence blocky/smooth dans le même chunk
- Buffer séparé pour les triangles smooth
### Phase 6 - Ray tracing hybride [A FAIRE]
- BLAS par chunk (depuis le mesh greedy), TLAS par frame
- RT Shadows via ray queries (compute shader)
- RT AO (4-8 rayons, courte portée)
- Fallback shadow maps / SSAO si RT non disponible
## Métriques cibles
| Métrique | Cible |
|----------|-------|
| FPS 1440p | > 60 fps, monde 512x512x128 |
| Meshing GPU | < 200 us par chunk 32^3 |
| Re-mesh | < 1 frame (16ms) pour 1 chunk |
| Mémoire GPU | < 500 Mo pour 512x512x128 |
| RT shadows + AO | < 4ms en 1440p |
| Draw calls | < 100 (hors post-process) |
## Conventions
- Namespaces : tout le code voxel est dans `namespace voxel`
- Chunks : 32x32x32, configurable via `CHUNK_SIZE`
- Coordonnées : Y = haut, monde infini en X/Z, hashmap sparse
- Matériaux : palette de 256, index 0 = air (vide)
- Faces : 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z