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.
This commit is contained in:
parent
dac63e3be5
commit
afb86446cd
4 changed files with 526 additions and 673 deletions
564
CLAUDE.md
564
CLAUDE.md
|
|
@ -36,7 +36,8 @@ bvle-voxels/
|
||||||
│ ├── voxelShadowCS.hlsl # Compute shader RT shadows + raw AO (inline ray queries, Phase 6.2+6.3)
|
│ ├── voxelShadowCS.hlsl # Compute shader RT shadows + raw AO (inline ray queries, Phase 6.2+6.3)
|
||||||
│ ├── voxelAOBlurCS.hlsl # Compute shader bilateral AO blur (separable H/V, Phase 6.3)
|
│ ├── voxelAOBlurCS.hlsl # Compute shader bilateral AO blur (separable H/V, Phase 6.3)
|
||||||
│ └── voxelAOApplyCS.hlsl # Compute shader AO apply + tone mapping + saturation (Phase 6.3 + 7)
|
│ └── voxelAOApplyCS.hlsl # Compute shader AO apply + tone mapping + saturation (Phase 6.3 + 7)
|
||||||
└── CLAUDE.md
|
├── CLAUDE.md
|
||||||
|
└── TROUBLESHOOTING.md # Pièges techniques, debugging, APIs Wicked
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
@ -72,19 +73,14 @@ Le build copie automatiquement :
|
||||||
|
|
||||||
### Backend graphique
|
### 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 :
|
Wicked Engine utilise **DX12 par défaut sur Windows**, Vulkan sur Linux. Les shaders sont écrits en **HLSL** et compilés via DXC. Pour forcer Vulkan sur Windows, passer `"vulkan"` en argument de ligne de commande.
|
||||||
- `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
|
### 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()`.
|
`VoxelRenderPath` hérite de `wi::RenderPath3D`. Le rendu voxel utilise ses propres render targets (`voxelRT_`, `voxelDepth_`) et est exécuté dans `Render()` sur un **command list dédié**. Le résultat est 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`).
|
**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.
|
||||||
|
|
||||||
Architecture correcte :
|
|
||||||
```
|
```
|
||||||
Render() → RenderPath3D::Render() // Wicked rend sa scène
|
Render() → RenderPath3D::Render() // Wicked rend sa scène
|
||||||
→ device->BeginCommandList() // Nouveau cmd list
|
→ device->BeginCommandList() // Nouveau cmd list
|
||||||
|
|
@ -95,152 +91,7 @@ Compose() → RenderPath3D::Compose() // Wicked affiche son résultat
|
||||||
|
|
||||||
La caméra est gérée manuellement dans `Update()` en écrivant directement `camera->Eye`, `camera->At` (direction LookTo), `camera->Up`.
|
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
|
> See `TROUBLESHOOTING.md` for the detailed Wicked API reference table, shader binding pitfalls, DX12 resource state management, and debugging guides.
|
||||||
|
|
||||||
| 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
|
|
||||||
```
|
|
||||||
|
|
||||||
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 — NON NÉCESSAIRES en pratique** :
|
|
||||||
|
|
||||||
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*(...);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
## Détails d'implémentation
|
||||||
|
|
||||||
|
|
@ -255,403 +106,84 @@ Les shaders custom doivent respecter le **binding model de Wicked Engine** :
|
||||||
### PackedQuad (64 bits = 8 octets par quad)
|
### PackedQuad (64 bits = 8 octets par quad)
|
||||||
|
|
||||||
```
|
```
|
||||||
[5:0] position X (0-63)
|
[5:0] position X (0-63) [23:18] width (1-32)
|
||||||
[11:6] position Y (0-63)
|
[11:6] position Y (0-63) [29:24] height (1-32)
|
||||||
[17:12] position Z (0-63)
|
[17:12] position Z (0-63) [32:30] face (0-5)
|
||||||
[23:18] width (1-32)
|
[40:33] material ID [48:41] blendMatID
|
||||||
[29:24] height (1-32)
|
[59:49] chunkIndex (11 bits) [63:60] blendEdges (4 bits)
|
||||||
[32:30] face (0-5 : +X,-X,+Y,-Y,+Z,-Z)
|
|
||||||
[40:33] material ID
|
|
||||||
[48:41] blendMatID (8 bits, matériau voisin pour height-based blending)
|
|
||||||
[59:49] chunkIndex (11 bits, utilisé par GPU mesh path pour lookup GPUChunkInfo)
|
|
||||||
[63:60] blendEdges (4 bits : +U(0), -U(1), +V(2), -V(3) — bords avec matériau différent)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Binary Greedy Mesher (CPU, `VoxelMesher.cpp`)
|
### Binary Greedy Mesher (CPU, `VoxelMesher.cpp`)
|
||||||
|
|
||||||
1. **Masques binaires** : pour chaque axe (X,Y,Z), `solid[u][v]` = bitmask 32 bits de voxels solides
|
Masques binaires par axe, face culling par shift/XOR, greedy merge rectangulaire par tranche de profondeur.
|
||||||
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`)
|
### Génération procédurale (`VoxelWorld.cpp`)
|
||||||
|
|
||||||
- Perlin noise 3D (permutation-based, seed configurable)
|
Perlin noise 3D, fBm 5 octaves (2 en animation), caves 3D, matériaux par altitude. Chunks Y=0..7. Animation 60 Hz via `regenerateAnimated()` parallélisé avec `wi::jobsystem`.
|
||||||
- fBm 5 octaves pour le heightmap (génération initiale), 2 octaves en animation (perf)
|
|
||||||
- Caves : `|fbm(x,y,z)| < threshold` en 3D (désactivées en mode animation)
|
|
||||||
- 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)
|
|
||||||
- Animation 60 Hz : `regenerateAnimated()` parallélise génération + pack GPU fusionnés via `wi::jobsystem`
|
|
||||||
|
|
||||||
### Renderer (`VoxelRenderer.cpp`)
|
### Renderer (`VoxelRenderer.cpp`)
|
||||||
|
|
||||||
- **Triple-mode VS** : CPU path (`flags=0`), MDI path (`flags & 1`), GPU mesh path (`flags & 2`)
|
- **Triple-mode VS** : CPU path, MDI path, GPU mesh path (défaut)
|
||||||
- **GPU mesh path (actif par défaut)** : compute shader `voxelMeshCS` génère les quads 1×1, `DrawInstanced` avec readback 1-frame-delay du compteur atomique
|
- **GPU mesh** : compute shader `voxelMeshCS` → barrier UAV→SRV → `DrawInstanced` (readback 1-frame-delay)
|
||||||
- **Mega-buffer** : tous les quads de tous les chunks dans un seul `StructuredBuffer<PackedQuad>` (2M quads, 16 MB) — utilisé en mode CPU/MDI
|
- **Vertex pulling** via `SV_VertexID`, pas de vertex buffer classique
|
||||||
- **Vertex pulling** : le VS lit le quad buffer via `SV_VertexID`, pas de vertex buffer classique
|
- **Per-chunk info** : `StructuredBuffer<GPUChunkInfo>` (80 bytes/chunk)
|
||||||
- **Pipeline** : PSO avec `RSTYPE_FRONT` (backface cull), `DSSTYPE_DEFAULT` (depth test), `BSTYPE_OPAQUE`
|
- **Height-based blending** (Phase 3) : PS lit `voxelDataBuffer` (t3), winner-takes-all heightmap, corner attenuation
|
||||||
- **Per-chunk info** : `StructuredBuffer<GPUChunkInfo>` (80 bytes/chunk) avec worldPos, quadOffset, faceOffsets[6], faceCounts[6]
|
- **Render targets propres** : `voxelRT_` (R8G8B8A8) + `voxelDepth_` (D32_FLOAT)
|
||||||
- **Push constants** (b999, 48 bytes) : chunkIndex + quadOffset + flags (bit 0 = MDI mode, bit 1 = GPU mesh mode)
|
- **CPU profiling** : `ProfileAccum` avec moyennes toutes les 5s
|
||||||
- **CPU culling** : frustum AABB (`wi::primitive::Frustum`) + backface par face group (camera vs AABB) — mode MDI uniquement
|
|
||||||
- **MDI rendering** (Phase 2.2) : un seul `DrawInstancedIndirectCount` remplace la boucle per-chunk. Push constant = `chunkIndex | (faceIndex << 16)`, le VS reconstruit quadOffset depuis GPUChunkInfo
|
|
||||||
- **Per-face-group draws** (Phase 2.1 fallback) : jusqu'à 6 `DrawInstanced` par chunk visible
|
|
||||||
- **Textures** : texture array 2D (256x256, 5 layers) générée procéduralement, triplanar mapping dans le PS. Alpha = heightmap procédural pour blending
|
|
||||||
- **Height-based blending** (Phase 3) : le PS lit directement `voxelDataBuffer` (SRV t3) pour lookup des matériaux voisins per-pixel. Winner-takes-all : le matériau avec la heightmap la plus haute gagne 100%. Transitions nettes mais forme organique dessinée par les heightmaps. Corner attenuation subtractive (param=0.80). Mode debug blend (F4)
|
|
||||||
- **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`
|
|
||||||
- **Frustum planes** : extraction Gribb-Hartmann dans le CB pour le compute shader de cull
|
|
||||||
- **GPU timestamp queries** : 6 slots (cull begin/end, draw begin/end, mesh begin/end)
|
|
||||||
- **CPU profiling** : `ProfileAccum` avec moyennes toutes les 5s dans le backlog (Regenerate, UpdateMeshes, VoxelPack, GPU Upload, GPU Dispatch, Render, Frame)
|
|
||||||
|
|
||||||
## Phases de développement (spec)
|
## Phases de développement
|
||||||
|
|
||||||
### Phase 1 - Setup et meshing de base [FAIT]
|
### Phase 1 - Setup et meshing de base [FAIT]
|
||||||
|
|
||||||
- Fork Wicked Engine, structure de modules
|
Fork Wicked Engine, VoxelWorld procédural, Binary Greedy Mesher CPU (~300K quads), rendu vertex pulling, caméra libre, crash handler SEH.
|
||||||
- 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 [FAIT]
|
### Phase 2 - Performance GPU [FAIT]
|
||||||
|
|
||||||
Découpée en sous-phases pour isoler les sources de bugs potentiels :
|
- **2.1** : Mega-buffer + CPU frustum/backface cull + per-face DrawInstanced
|
||||||
|
- **2.2** : CPU-filled indirect args + DrawInstancedIndirectCount (MDI)
|
||||||
#### Phase 2.1 - Mega-buffer + CPU cull + per-face DrawInstanced [FAIT]
|
- **2.3** : GPU compute culling (0.006 ms / 168 chunks)
|
||||||
|
- **2.4** : GPU compute mesher benchmark (CPU 277ms vs GPU 5.3ms)
|
||||||
- Mega-buffer : tous les quads dans un seul SRV, packés par chunk
|
- **2.5** : GPU meshing production + CPU optimisations (fused regen+pack, memcpy, dirty cache)
|
||||||
- Tri par face group dans le mesher (`faceOffsets[6]`, `faceCounts[6]`)
|
- **Résultat** : 80-110 FPS avec animation 60 Hz, 700+ FPS statique
|
||||||
- CPU frustum culling (AABB vs `wi::primitive::Frustum`)
|
|
||||||
- CPU backface culling par face group (camera.Eye vs chunk AABB)
|
|
||||||
- Per-face-group `DrawInstanced` (max 6 draws par chunk visible)
|
|
||||||
- `GPUChunkInfo` StructuredBuffer pour lookup VS
|
|
||||||
|
|
||||||
#### Phase 2.2 - CPU-filled indirect args + DrawInstancedIndirectCount [FAIT]
|
|
||||||
|
|
||||||
- Le CPU remplit `IndirectDrawArgs[]` avec la même logique que 2.1 (frustum + backface)
|
|
||||||
- Le CPU écrit le draw count
|
|
||||||
- Upload des deux buffers vers le GPU (sans barriers explicites — promotion implicite)
|
|
||||||
- Un seul `DrawInstancedIndirectCount` remplace la boucle per-chunk
|
|
||||||
- Le VS décode `chunkIndex | (faceIndex << 16)` depuis le push constant et reconstruit le quadOffset
|
|
||||||
- **Intérêt** : teste le MDI rendering SANS compute shader (isole les problèmes de barriers)
|
|
||||||
- **Pièges résolus** :
|
|
||||||
- `IndirectDrawArgs` fait 20 bytes (pas 16) — voir point 7 dans "Shaders custom — PIÈGES IMPORTANTS"
|
|
||||||
- `SV_VertexID` n'inclut pas `startVertexLocation` avec ExecuteIndirect — voir point 8
|
|
||||||
- Pas de barriers explicites nécessaires — voir point 9
|
|
||||||
|
|
||||||
#### Phase 2.3 - GPU compute culling [FAIT]
|
|
||||||
|
|
||||||
- Le compute shader `voxelCullCS.hlsl` remplace le CPU pour remplir les indirect args
|
|
||||||
- Barriers DX12 : UNDEFINED → UAV (pre-compute) → INDIRECT_ARGUMENT (post-compute)
|
|
||||||
- GPU timestamp queries actifs (GPU Cull ~0.006 ms pour 168 chunks)
|
|
||||||
- **Pièges résolus** :
|
|
||||||
- `PushConstants` DOIT être appelé APRÈS `BindPipelineState` — voir point 10
|
|
||||||
- Compute shader corrigé : push constant packing + startVertexLocation=0 — voir points 7-8
|
|
||||||
- `ResourceState::UNDEFINED` = COMMON en Wicked (valeur 0), déclenche `DiscardResource()` — OK pour les buffers réécrits
|
|
||||||
|
|
||||||
#### Phase 2.4 - GPU compute mesher (benchmark) [FAIT]
|
|
||||||
|
|
||||||
- Le compute shader `voxelMeshCS.hlsl` fait le meshing 1×1 sur GPU (1 thread par voxel, 8×8×8 thread groups)
|
|
||||||
- Benchmark automatique au premier frame après génération du monde (mode CPU fallback)
|
|
||||||
- Résultats (168 chunks, Ryzen 7 9800X3D + RX 9070 XT) :
|
|
||||||
- CPU greedy: 277 ms, 358K quads → greedy merge réduit les quads de 6.8×
|
|
||||||
- GPU baseline (1×1): 5.3 ms, 2.43M quads → 52× plus rapide que CPU
|
|
||||||
- GPU greedy merge non implémenté (pourrait combiner vitesse GPU + réduction de quads)
|
|
||||||
- Le benchmark est one-shot : state machine IDLE → DISPATCH → READBACK → DONE
|
|
||||||
|
|
||||||
#### Phase 2.5 - GPU meshing production + optimisations perf [FAIT]
|
|
||||||
|
|
||||||
- **GPU meshing en production** : remplace le CPU greedy mesher comme pipeline par défaut
|
|
||||||
- `voxelMeshCS.hlsl` : chunkIndex encodé dans les bits [63:49] de chaque quad (11 bits)
|
|
||||||
- `voxelVS.hlsl` : mode `flags & 2` extrait le chunkIndex depuis le quad, lookup `GPUChunkInfo`
|
|
||||||
- `VoxelRenderer` : dispatch compute shader → barrier UAV→SRV → `DrawInstanced`
|
|
||||||
- Readback 1-frame-delay du compteur atomique pour le vertex count
|
|
||||||
- Le `gpuQuadBuffer_` a les bind flags `UNORDERED_ACCESS | SHADER_RESOURCE`
|
|
||||||
- **Optimisations perf CPU** (profilées et mesurées) :
|
|
||||||
- **VoxelPack par memcpy** : `sizeof(VoxelData) == 2`, donc `voxels[]` est directement compatible avec le format GPU (uint16 pairs). Remplace la boucle bit-shift (28ms → <1ms)
|
|
||||||
- **Cache dirty** : `packedVoxelCache_` ne se repack que quand les chunks changent, pas chaque frame
|
|
||||||
- **Fused regenerate+pack** : `regenerateAnimated()` accepte un pointeur de destination, chaque job parallèle fait generate + memcpy dans le même thread. Élimine la double itération du hashmap et le pack séquentiel (6ms → 0ms)
|
|
||||||
- **Skip GPU dispatch** : `gpuMeshDirty_` flag empêche le re-dispatch/upload quand rien n'a changé
|
|
||||||
- **Upload conditionnel** : `chunkInfoBuffer_` ne se re-upload que quand `chunkInfoDirty_`
|
|
||||||
- **Animation allégée** : 2 octaves fBm (au lieu de 5) + pas de caves en mode animation (54ms → 8ms)
|
|
||||||
- **Résultats finaux** (171 chunks, Ryzen 7 9800X3D + RX 9070 XT, animation 60 Hz) :
|
|
||||||
- Regenerate: 8.7ms (parallèle, 2 octaves)
|
|
||||||
- VoxelPack: 0ms (fusionné dans regenerate)
|
|
||||||
- GPU Upload: 4.5ms (~11 MB voxel data)
|
|
||||||
- GPU Dispatch: 0.1ms (171 × 64 thread groups)
|
|
||||||
- Frame total: ~9ms → **80-110 FPS** avec animation terrain 60 Hz
|
|
||||||
- Sans animation: **700+ FPS**
|
|
||||||
|
|
||||||
### Phase 3 - Texture blending [FAIT]
|
### Phase 3 - Texture blending [FAIT]
|
||||||
|
|
||||||
Approche **PS-based** : le pixel shader lit directement les données voxel (pas de pré-encodage dans les quads). Voir `blending_experiments.md` pour le détail des itérations.
|
PS-based heightmap blending, winner-takes-all, corner attenuation subtractive. GPU mesh path uniquement.
|
||||||
|
|
||||||
- **Heightmaps procéduraux** dans le canal alpha de chaque texture de matériau (5 matériaux, paramètres freq/contrast différents)
|
|
||||||
- **PS neighbor lookup** (`voxelPS.hlsl`) : bind `voxelDataBuffer` à `t3`, `chunkInfoBuffer` à `t2`. Lit les matériaux voisins per-pixel via `readVoxelMat(coord, chunkIdx)`
|
|
||||||
- **Stair priority** : pour chaque bord, vérifie `pos + edgeDir + normalDir` en premier (le bloc qui masque visuellement le coin), puis fallback `pos + edgeDir`
|
|
||||||
- **2 axes indépendants** : U et V sont traités séparément avec nearest-edge detection via `sign(faceFrac - 0.5)`
|
|
||||||
- **Winner-takes-all heightmap** : `mainScore = h_main + bias`, `neighScore = h_neigh - bias`, `bias = 0.5 - weight`. Le matériau avec le score le plus haut gagne à 100%. Sharpness=16 pour anti-aliasing
|
|
||||||
- **Corner attenuation subtractive** : `xAdj = xEdge - saturate(yEdge - 0.80)` — réduit le blend aux coins où les deux axes se croisent
|
|
||||||
- **Zone de blend** : 0.25 voxels depuis chaque bord (50% de la face)
|
|
||||||
- **CB** : `blendEnabled` (float, 1.0 en GPU mesh path, 0.0 sinon) + `debugBlend` (float, toggle F4)
|
|
||||||
- **VS** (`voxelVS.hlsl`) : passe `chunkIndex` (nointerpolation uint) au PS pour les lookups voxel
|
|
||||||
- **GPU mesher** (`voxelMeshCS.hlsl`) : simplifié (pas de blend computation), encode seulement `chunkIndex` dans les bits [27:17] du quad
|
|
||||||
- **Mode debug** (F4) : visualise les zones de blend (rouge=U, bleu=V, vert=pas de blend, rouge vif=data mismatch)
|
|
||||||
- **Fonctionne uniquement en GPU mesh path** (1×1 quads) ; CPU/MDI paths ont `blendEnabled=0`
|
|
||||||
|
|
||||||
### Phase 4 - Toping [EN COURS]
|
### Phase 4 - Toping [EN COURS]
|
||||||
|
|
||||||
Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour adoucir les transitions entre blocs.
|
- **4.1** [FAIT] : TopingSystem infrastructure, 4-bit adjacency, priority-based, stone wedges + grass tufts
|
||||||
|
- **4.2** [FAIT] : Shaders dédiés, vertex pulling instancié, half-Lambert + translucency vegetation
|
||||||
#### Phase 4.1 - Infrastructure TopingSystem [FAIT]
|
- **4.3** [A FAIRE] : Plus de types, LOD, animation vent, compute shader instances
|
||||||
|
|
||||||
- **TopingSystem** (`TopingSystem.h/.cpp`) : data structures + mesh generation + instance collection
|
|
||||||
- **4-bit adjacency bitmask** : pour chaque face +Y exposée, vérifie 4 voisins cardinaux (±X, ±Z) pour même matériau avec +Y exposée → 16 variantes
|
|
||||||
- **Priority-based adjacency** : `TopingDef.priority` détermine quel toping cède aux frontières de matériaux. Grass (priority=1) génère des biseaux par-dessus stone (priority=0)
|
|
||||||
- **Mesh par matériau** :
|
|
||||||
- **Stone** : wedge cross-section (outer wall + slope) + corner fills + caps aux terminaisons
|
|
||||||
- **Grass** : brins d'herbe individuels groupés en touffes, 2 segments courbés, double-sided
|
|
||||||
- ~191K instances pour ~170 chunks
|
|
||||||
|
|
||||||
#### Phase 4.2 - Rendu GPU + shading végétal [FAIT]
|
|
||||||
|
|
||||||
- **Shaders dédiés** : `voxelTopingVS.hlsl` (vertex pulling instancié) + `voxelTopingPS.hlsl` (shading par matériau)
|
|
||||||
- **Vertex pulling** : `StructuredBuffer<TopingVertex>` (t4) + `StructuredBuffer<float3>` (t5 instances)
|
|
||||||
- **Push constants** : `vertexOffset`, `instanceOffset`, `materialID` réutilisent les 3 premiers champs de b999
|
|
||||||
- **Per-group DrawInstanced** : instances triées par (type, variant), un `DrawInstanced` par groupe contigu
|
|
||||||
- **Render pass séparé** avec `LoadOp::LOAD` : topings rendus après voxels, préservent RT et depth
|
|
||||||
- **PSO** : même rasterizer/depth/blend que les voxels (`RSTYPE_FRONT`, `DSSTYPE_DEFAULT`, `BSTYPE_OPAQUE`)
|
|
||||||
- **Shading végétal stylisé** (inspiré Airborn Trees, `voxelTopingPS.hlsl`) :
|
|
||||||
- **Half-Lambert wrap lighting** : `(N·L * 0.5 + 0.5)²` — enveloppe la lumière, pas de terminator dur
|
|
||||||
- **Translucency** : `dot(V, L) * (1 - NdotL) * 0.4` — lumière traversant les brins fins à contre-jour
|
|
||||||
- **Ambient chaud** : `(0.22, 0.28, 0.20)` — plus lumineux et verdâtre que l'ambient stone
|
|
||||||
- **Stone** : Lambert classique identique aux voxels (branchement sur `materialID == 3`)
|
|
||||||
- **Génération de touffes d'herbe** (`TopingSystem.cpp`) :
|
|
||||||
- **Tufts** : clusters de 3–9 brins partageant un centre commun (scatter ±0.03)
|
|
||||||
- **Position des touffes** : hash-driven le long du bord + inset quadratique 0.0–0.30 du bord
|
|
||||||
- **Par-tuft personality** : heightScale (0.20–1.0), leanScale (0.3–1.8), blade count (3–9)
|
|
||||||
- **Par-brin variety** : hauteur, largeur, angle (±55° fan + jitter), courbure (midLeanRatio 0.08–0.43)
|
|
||||||
- **Hash déterministe** : `hashF(a,b,c)` golden-ratio based pour reproductibilité
|
|
||||||
- **Stone corner fills** : triangle de pente diagonal aux coins où deux bords ouverts se rejoignent
|
|
||||||
- **Stone caps** : triangle fermant la section du biseau aux terminaisons de strip
|
|
||||||
- **Pièges résolus** :
|
|
||||||
- **Winding CW** : `emitTri()` auto-corrige le winding via `dot(geom, desired) > 0` → swap B↔C
|
|
||||||
- **Slope normal = inward + up** : utiliser `(e.ix, e.iz)`, PAS `(e.nx, e.nz)`
|
|
||||||
- **sunDirection** : `L = normalize(-sunDirection.xyz)` (direction de voyage → direction vers le soleil)
|
|
||||||
|
|
||||||
#### Phase 4.3 - Polish et extensions [A FAIRE]
|
|
||||||
|
|
||||||
- Plus de types de topings (neige, mousse, etc.)
|
|
||||||
- LOD : supprimer les topings à distance
|
|
||||||
- Animation subtile (vent sur l'herbe via vertex shader)
|
|
||||||
- Optimisation : compute shader pour le instance collection
|
|
||||||
|
|
||||||
### Phase 5 - Rendu smooth [EN COURS]
|
### Phase 5 - Rendu smooth [EN COURS]
|
||||||
|
|
||||||
#### Phase 5.1 - Naive Surface Nets CPU [FAIT]
|
- **5.1** [FAIT] : Naive Surface Nets CPU, SDF binaire, cross-chunk connectivity, smooth/blocky boundary
|
||||||
|
- **5.2** [FAIT] : Smooth vertex normals, geometric normals triplanar, optimisations CPU (560ms → 17ms)
|
||||||
- **Algorithme** : Naive Surface Nets (dual contouring simplifié) dans `SmoothMesher::meshChunk()`
|
- **5.3** [A FAIRE] : GPU compute Surface Nets
|
||||||
- **SDF binaire** : solid = -1, empty = +1 (pas de distance field continu)
|
- **5.4** [A FAIRE] : SDF lissé, LOD, pipeline asynchrone
|
||||||
- **Vertex placement** : centroïde des edge crossings pour chaque cellule à la surface
|
|
||||||
- **Matériaux smooth** : SmoothStone (mat 6, `FLAG_SMOOTH`) et Snow (mat 5, `FLAG_SMOOTH`)
|
|
||||||
- **Matériaux blocky** : Stone (mat 3), Grass (mat 1), Dirt (mat 2), Sand (mat 4)
|
|
||||||
- **SmoothVertex** (32 bytes) : position, face normal, materialID, secondaryMat, blendWeight, chunkIndex
|
|
||||||
- **Shaders dédiés** : `voxelSmoothVS.hlsl` (vertex pulling t6) + `voxelSmoothPS.hlsl` (triplanar + blending)
|
|
||||||
- **Render pass séparé** avec `LoadOp::LOAD` : smooth rendu après voxels+topings, préserve RT et depth
|
|
||||||
|
|
||||||
**Cross-chunk connectivity** :
|
|
||||||
- **PAD=2** dans la grille SDF pour accéder aux cellules [-1..CHUNK_SIZE]
|
|
||||||
- **Vertex range étendu** : `[-1, CHUNK_SIZE)` au lieu de `[0, CHUNK_SIZE)` — les cellules au bord du chunk voisin génèrent des vertices
|
|
||||||
- **Canonical ownership** : chaque edge est émise par un seul chunk (celui contenant le grid point inférieur), pas de duplication
|
|
||||||
|
|
||||||
**Smooth↔blocky boundary** :
|
|
||||||
- **`hasSmooth` filter étendu** : génère des vertices si la cellule OU une cellule 6-connectée adjacente contient un coin smooth. Sans cet élargissement, les cellules à la frontière smooth↔blocky (100% blocky mais adjacentes à du smooth) n'ont pas de vertex → les quads de connexion ne peuvent pas être émis → trou de génération
|
|
||||||
- **Per-axis boundary clamping** : les vertices aux frontières smooth↔blocky sont clampés vers la grille entière (empêche le mesh smooth de dépasser sur les faces blocky)
|
|
||||||
- **GPU mesher** : les voxels smooth sont traités comme solides dans `isNeighborAir()` — les faces blocky ne sont pas émises vers les voxels smooth (le mesh smooth couvre la frontière)
|
|
||||||
|
|
||||||
**Face normals — PIÈGES MAJEURS** :
|
|
||||||
- **Face normals, pas SDF gradient** : le SDF binaire donne des gradients à 45° aux marches, causant du stretching triplanar. Les face normals (cross product des edges du triangle) sont géométriquement correctes.
|
|
||||||
- **Orientation par axe de l'edge** : chaque quad vient d'une edge X, Y ou Z. La direction `solid→empty` est connue. On vérifie que la composante de la face normal sur cet axe a le bon signe, sinon flip.
|
|
||||||
- **Y-axis winding inversé** : les sharing cells Y sont arrangées différemment de X et Z. Le winding naturel du quad Y est opposé → `if (axis == 1) useWindingA = !useWindingA;`
|
|
||||||
- **SDF gradient dot product** : NE PAS utiliser pour orienter les normals (échoue quand le gradient est nul ou ambigu avec SDF binaire)
|
|
||||||
- **Centroid SDF sampling** : NE PAS utiliser non plus (les deux côtés arrondissent souvent au même voxel)
|
|
||||||
|
|
||||||
**Material blending (per-pixel, same as blocky PS)** :
|
|
||||||
- **Dominant axis detection** : le PS smooth dérive un « face virtuelle » depuis la normale lisse. L'axe avec la plus grande composante `|N|` détermine la face dominante (0-5). Cela donne accès aux mêmes tables `faceNormals`, `faceUDirs`, `faceVDirs` que le PS blocky
|
|
||||||
- **Même voxelCoord** : `floor(worldPos - normalDir * 0.001)` — tiny offset le long de la normale dominante (PAS `N * 0.5` qui est trop large et tombe dans le mauvais voxel)
|
|
||||||
- **Même `getNeighborMat()` avec stair priority** : vérifie `pos + edgeDir + normalDir` en premier (le bloc qui masque visuellement l'arête), puis fallback `pos + edgeDir`
|
|
||||||
- **Face-aligned U/V** : `frac(dot(worldPos, uDir))` / `frac(dot(worldPos, vDir))` — position fractionnaire dans le voxel selon les tangentes de la face dominante
|
|
||||||
- **Même blend zone (0.25)**, corner attenuation subtractive, winner-takes-all heightmap avec sharpness=16
|
|
||||||
- **Même bleedMask/resistBleedMask** checks via CB
|
|
||||||
- **PIÈGE** : NE PAS utiliser les 3 axes world-space avec un filtre `dirDotN > 0.6` — ça ne filtre pas correctement les voisins souterrains et donne des blends incorrects. La dérivation d'un face dominant + U/V alignés est la seule approche correcte
|
|
||||||
|
|
||||||
**Debug scene smooth** :
|
|
||||||
- Lancé avec `BVLEVoxels.exe debugsmooth`
|
|
||||||
- 11 configurations isolées dans un seul chunk : SmoothStone↔Grass, SmoothStone↔Dirt, SmoothStone↔Sand, SmoothStone↔Stone, Snow↔Grass, Snow↔Sand, références blocky (Sand↔Dirt, Grass↔Dirt), escalier SmoothStone, patch smooth entouré de grass, bloc smooth isolé
|
|
||||||
|
|
||||||
#### Phase 5.2 - Smooth normals + optimisations perf [FAIT]
|
|
||||||
|
|
||||||
- **Smooth vertex normals** : accumulation area-weighted des face normals dans chaque vertex indexé, puis normalisation. Donne un éclairage Gouraud lisse sans géométrie additionnelle
|
|
||||||
- **Geometric normals pour triplanar** : le PS utilise `ddx`/`ddy` du worldPos pour reconstruire la normal géométrique (face) pour les poids triplanar, la smooth normal pour le lighting uniquement. Empêche le stretching de textures causé par les normals lissées
|
|
||||||
- **Depth bias smooth PSO** : rasterizer avec `depth_bias = 1` pour résoudre le z-fighting smooth↔blocky aux frontières
|
|
||||||
- **Surface-only vertex extension** : le filtre `hasSmooth` étendu vérifie aussi que la cellule est sur la surface (`hasPos && hasNeg`) ET non entièrement souterraine. Empêche le smooth mesh de plonger dans le sous-sol
|
|
||||||
|
|
||||||
**Optimisations CPU (560ms → 17ms = 33× plus rapide)** :
|
|
||||||
- **Cache VoxelData dans la grille** : `voxelGrid[]` stocke VoxelData aux côtés du SDF, élimine tous les `readVoxel` redondants dans le boundary clamping, material counting, surface check
|
|
||||||
- **Pré-cache 27 chunks voisins** : `neighborChunks[3][3][3]` rempli avant le grid fill. `readVoxelFast()` utilise un accès direct au tableau du chunk voisin au lieu de `world.getVoxel()` (hashmap lookup). Élimine ~14K hashmap lookups par chunk smooth
|
|
||||||
- **Suppression `computeNormal` mort** : la fonction SDF gradient (6 readVoxel/vertex) était écrasée par les smooth normals. Code mort supprimé
|
|
||||||
- **Early exit `containsSmooth`** : flag posé pendant `generateChunk()`. `meshChunk()` vérifie ce chunk + 26 voisins (27 hashmap lookups) avant le grid fill coûteux (46K voxel reads). Skip ~70% des chunks
|
|
||||||
- **Dilation smoothNear** : grille pré-dilatée (smooth + face-neighbors) remplace le check hasSmooth étendu. 8 lookups/cell au lieu de 56 (7× moins dans la boucle la plus chaude)
|
|
||||||
- **Thread-local scratch buffers** : `SmoothScratch` (~600 KB) alloué une fois par thread, réutilisé entre les appels. Élimine malloc/free par chunk
|
|
||||||
- **Parallélisation `wi::jobsystem`** : tous les chunks meshés en parallèle sur tous les cœurs CPU
|
|
||||||
- **Staging vectors persistants** : `smoothStagingVerts_` réutilisé entre frames, évite les allocations de vecteurs
|
|
||||||
|
|
||||||
**Optimisations TopingSystem** :
|
|
||||||
- **`collectInstancesParallel()`** : chaque chunk écrit dans un vecteur local, merge séquentiel. Élimine la contention
|
|
||||||
- **Staging vectors persistants** : `topingSorted_`, `topingGpuInsts_` réutilisés entre frames
|
|
||||||
|
|
||||||
**Résultats animation (648 chunks, Ryzen 7 9800X3D + RX 9070 XT)** :
|
|
||||||
- SmoothMesh: 560ms → 17ms (parallèle, dilation, cache)
|
|
||||||
- SmoothUpload: 13ms → 4ms (staging persistant)
|
|
||||||
- TopingCollect: 58ms → 6.5ms (parallèle)
|
|
||||||
- TopingUpload: 7.5ms → 1.2ms (bug fix timing + staging persistant)
|
|
||||||
- Frame total: 662ms → 58ms (1.5 → 17 FPS avec animation terrain)
|
|
||||||
|
|
||||||
#### Phase 5.3 - GPU compute Surface Nets [A FAIRE]
|
|
||||||
|
|
||||||
- Compute shader pour SDF grid fill + vertex generation + quad emission
|
|
||||||
- Élimine le CPU bottleneck restant (17ms → <1ms estimé)
|
|
||||||
- Pattern similaire au GPU mesher blocky (Phase 2.4-2.5)
|
|
||||||
- Readback 1-frame-delay du compteur atomique pour le vertex count
|
|
||||||
|
|
||||||
#### Phase 5.4 - Polish [A FAIRE]
|
|
||||||
|
|
||||||
- SDF lissé (distance field approximatif au lieu de binaire ±1)
|
|
||||||
- LOD : réduction de triangles à distance
|
|
||||||
- Pipeline asynchrone : double-buffer GPU resources, CPU frame N prépare pendant que GPU rend frame N-1
|
|
||||||
|
|
||||||
### Phase 6 - Ray tracing hybride [EN COURS]
|
### Phase 6 - Ray tracing hybride [EN COURS]
|
||||||
|
|
||||||
#### Phase 6.1 - Infrastructure RT (Normal RT + BLAS/TLAS) [FAIT]
|
- **6.1** [FAIT] : Normal RT (MRT), BLAS extraction CS, 3 BLAS (blocky+smooth+topings), TLAS
|
||||||
|
- **6.2** [FAIT] : RT shadows (3 jittered rays, TMin adaptatif, colored shadows)
|
||||||
|
- **6.3** [FAIT] : RT AO (4 cosine-weighted rays, IGN + Cranley-Patterson, temporal accumulation, bilateral blur)
|
||||||
|
- **6.4** [A FAIRE] : Fallback shadow maps + SSAO
|
||||||
|
|
||||||
- **Normal render target (MRT)** : `voxelNormalRT_` (R16G16B16A16_SNORM) added as SV_TARGET1
|
### Phase 7 - Stylized Lighting [EN COURS]
|
||||||
- All 3 pixel shaders (voxelPS, voxelTopingPS, voxelSmoothPS) output `PSOutput` struct with `SV_TARGET0` (color) + `SV_TARGET1` (world-space normal)
|
|
||||||
- All 3 render passes (`render`, `renderTopings`, `renderSmooth`) use 3 `RenderPassImage` entries (color + normal + depth)
|
|
||||||
- **BLAS extraction compute shader** (`voxelBLASExtractCS.hlsl`) :
|
|
||||||
- Reads `gpuQuadBuffer_` (StructuredBuffer<PackedQuad>), extracts world-space float3 positions
|
|
||||||
- 1 thread per quad → 6 vertices (2 triangles), same unpack + winding logic as voxelVS.hlsl
|
|
||||||
- Output: `blasPositionBuffer_` (RWByteAddressBuffer, raw buffer), non-indexed triangles
|
|
||||||
- Dispatched after GPU mesh pass, only when quad count changes
|
|
||||||
- **Blocky BLAS** : single BLAS from `blasPositionBuffer_` (all blocky quads as non-indexed triangles)
|
|
||||||
- `PREFER_FAST_BUILD` flag for quick rebuilds during animation
|
|
||||||
- Vertex format: R32G32B32_FLOAT, stride 12 bytes
|
|
||||||
- **Smooth BLAS** : single BLAS from `gpuSmoothVertexBuffer_` directly (no extraction needed)
|
|
||||||
- Position at offset 0, stride 32 bytes (SmoothVtx struct)
|
|
||||||
- Same `PREFER_FAST_BUILD` flag
|
|
||||||
- **Toping BLAS** : single BLAS from expanded toping vertices (mesh × instances → world-space float3)
|
|
||||||
- CPU-side expansion in `uploadTopingData()`: iterates sorted instances, transforms local vertices to world positions
|
|
||||||
- Dedicated `topingBLASPositionBuffer_` + `topingBLASIndexBuffer_` (separate from blocky)
|
|
||||||
- `PREFER_FAST_TRACE` flag (optimizes BVH traversal, important for 23M tris)
|
|
||||||
- ~23M triangles for ~153K instances (varies with blade count/segments)
|
|
||||||
- **TLAS** : 3 instances (blocky + smooth + topings), identity transforms (all positions are world-space)
|
|
||||||
- Instance buffer created via `CreateBuffer2` with pre-filled instance data (callback)
|
|
||||||
- `instance_mask = 0xFF` for both instances
|
|
||||||
- Recreated each rebuild (avoids `UpdateBuffer` on RAY_TRACING flagged buffers)
|
|
||||||
- **Lifecycle** : BLAS/TLAS rebuilt when geometry changes (quad count differs from previous frame)
|
|
||||||
- `rtDirty_` flag triggers rebuild on first frame
|
|
||||||
- Smooth BLAS auto-recreated when vertex count changes
|
|
||||||
- **HUD** : RT status line showing TLAS state + triangle counts for blocky/smooth
|
|
||||||
- **Pièges résolus** :
|
|
||||||
- **Index buffer obligatoire dans BLAS** : `CreateRaytracingAccelerationStructure` dans Wicked accède TOUJOURS `index_buffer` via `to_internal()` (ligne 4356 de `wiGraphicsDevice_DX12.cpp`), même pour de la géométrie non-indexée. Un `GPUBuffer` par défaut (invalide) cause un null deref à offset 0xd8. De plus, `index_count = 0` avec `IndexBuffer != 0` fait que DX12 interprète "0 triangles indexés" → BLAS vide. Solution : fournir un vrai sequential index buffer `[0,1,2,...]` avec `index_count = vertex_count` et `index_format = UINT32`
|
|
||||||
- **`CreateBuffer2` pour TLAS instance buffer** : les buffers avec `ResourceMiscFlag::RAY_TRACING` ne supportent pas `UpdateBuffer` (state mismatch). Utiliser `CreateBuffer2` avec callback pour pré-remplir les instances à la création
|
|
||||||
- **Memory barriers BLAS→TLAS→RT — PIÈGE MAJEUR** : `BuildRaytracingAccelerationStructure` est asynchrone GPU. Sans barriers :
|
|
||||||
- Le TLAS build peut s'exécuter avant que les BLAS ne soient terminés
|
|
||||||
- Les ray queries peuvent s'exécuter avant que le TLAS ne soit prêt
|
|
||||||
- Résultat : BLAS apparaît vide (zéro hits) sans aucun crash ni erreur
|
|
||||||
- Solution (pattern de `wiRenderer.cpp` lignes 5788, 5808) :
|
|
||||||
1. `GPUBarrier::Memory()` après tous les BLAS builds, avant le TLAS build
|
|
||||||
2. `GPUBarrier::Memory(&tlas_)` après le TLAS build, avant les ray queries
|
|
||||||
|
|
||||||
#### Phase 6.2 - RT Shadows [FAIT]
|
- **7.1** [FAIT] : Hemisphere ambient, colored shadows, rim light, tone mapping + saturation, screenshot mode
|
||||||
|
|
||||||
- **Compute shader** (`voxelShadowCS.hlsl`) avec inline ray queries (`RayQuery<>`, SM 6.5)
|
|
||||||
- Lit `voxelDepth_` (t0, D32→R32_FLOAT) + `voxelNormalRT_` (t1) + TLAS (t2)
|
|
||||||
- Reconstruit worldPos depuis depth via `inverseViewProjection` (ajouté au VoxelCB)
|
|
||||||
- 3 shadow rays jittered vers le soleil (cone 0.012 rad ≈ 0.7°) pour soft shadows
|
|
||||||
- `RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH` + `RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES` (shadow binaire)
|
|
||||||
- Surfaces back-facing (NdotL ≤ 0) : assombries sans ray trace (shadowFactor=0.45)
|
|
||||||
- **In-place modulation** : `RWTexture2D<float4>` sur `voxelRT_` (u0), chaque thread lit/modifie son pixel (pas de race)
|
|
||||||
- Colored shadows : `lerp(color, color * shadowTint, shadowAmount)` au lieu de simple darkening
|
|
||||||
- `voxelRT_` créé avec `UNORDERED_ACCESS` additionnel pour permettre l'écriture compute
|
|
||||||
- **Shadow origin bias — PIÈGE MAJEUR** :
|
|
||||||
- L'origine du shadow ray utilise `shadowOrigin = worldPos` (zéro bias position) au lieu du normal bias de l'AO
|
|
||||||
- Le normal bias (`worldPos + N * 0.15`) pousse l'origine AU-DESSUS des bases de brins d'herbe → gap entre le brin et son ombre
|
|
||||||
- Le light bias (`worldPos + L * offset`) crée aussi un gap (proportionnel au bias)
|
|
||||||
- **TMin adaptatif** résout le dilemme self-hit vs gap :
|
|
||||||
- `TMin = lerp(0.002, 0.10, 1.0 - abs(N.y))`
|
|
||||||
- Sol (N.y ≈ 1) → TMin=0.002 : ombres collées aux bases des brins
|
|
||||||
- Brins d'herbe (N.y ≈ 0) → TMin=0.10 : skip la propre géométrie du brin
|
|
||||||
- **Screen-space contact shadows ne fonctionnent PAS** pour l'herbe : les brins sont trop fins pour que le delta de profondeur NDC soit distinguable du bruit. Testé en 4 itérations (NDC comparison, world-space reconstruction, height filter) — tous échouent
|
|
||||||
- **Dispatch** : 8×8 thread groups, `ceil(w/8) × ceil(h/8)`, après les 3 render passes (blocky+topings+smooth)
|
|
||||||
- **Barriers** :
|
|
||||||
- Pre : `voxelDepth_` DEPTHSTENCIL→SHADER_RESOURCE + `voxelRT_` SHADER_RESOURCE→UAV
|
|
||||||
- Post : `voxelDepth_` SHADER_RESOURCE→DEPTHSTENCIL + `voxelRT_` UAV→SHADER_RESOURCE
|
|
||||||
- **Mode debug** (F5 × 2 = DBG) : rouge=shadow hit, vert=miss, bleu=back-facing, gris foncé=ciel
|
|
||||||
- **Toggle** : F5 cycle OFF→ON→DBG_SHADOW→DBG_AO→OFF
|
|
||||||
- **CB** : `inverseViewProjection` (float4x4) ajouté après `viewProjection` dans VoxelCB (HLSL + C++)
|
|
||||||
- **Push constants** : width, height, normalBias, maxDistance, debugMode
|
|
||||||
|
|
||||||
#### Phase 6.3 - RT AO [FAIT]
|
|
||||||
|
|
||||||
- **Intégré dans `voxelShadowCS.hlsl`** : 4 rayons AO hémisphère cosine-weighted + 3 rayons shadow jittered par pixel (7 total)
|
|
||||||
- **Distance-weighted AO** : `(1 - hitT/aoRadius)²` — falloff quadratique, valeurs continues au lieu de binaire hit/miss
|
|
||||||
- **Interleaved Gradient Noise (IGN)** : remplace le hash world-space pour le sampling. Bruit structuré screen-space avec excellentes propriétés spectrales (Jorge Jimenez, 2014)
|
|
||||||
- **Cranley-Patterson rotation** : `frac(IGN + frameIndex * φ)` — chaque frame explore de nouvelles directions de rayons. Golden ratio (φ ≈ 0.618) assure une couverture maximale de l'hémisphère au fil des frames
|
|
||||||
- **Accumulation temporelle** : `lerp(history, current, 0.05)` ≈ accumulation de ~20 frames
|
|
||||||
- `aoHistoryTexture_` (R8_UNORM) persiste entre frames
|
|
||||||
- `prevViewProjection` dans VoxelCB pour reprojection worldPos → UV du frame précédent
|
|
||||||
- Rejet si UV hors écran (disocclusion basique)
|
|
||||||
- `frameIndex` + `historyValid` dans les push constants
|
|
||||||
- Copy `aoRaw → aoHistory` entre le shadow CS et le blur (capture le signal pré-blur)
|
|
||||||
- **Bilateral blur séparable** (`voxelAOBlurCS.hlsl`) : 2 passes H+V, rayon 6 (kernel 13×13), edge-stopping sur depth + normals
|
|
||||||
- **Pipeline 5 passes** :
|
|
||||||
1. Shadow CS : shadow in-place sur `voxelRT_` + AO temporellement accumulé → `aoRawTexture_` (u1)
|
|
||||||
2. Copy : `aoRawTexture_` → `aoHistoryTexture_` (pour le frame suivant, pré-blur)
|
|
||||||
3. Blur H : `aoRawTexture_` → `aoBlurredTexture_` (bilateral, depth/normal edge-stopping)
|
|
||||||
4. Blur V : `aoBlurredTexture_` → `aoRawTexture_` (idem, direction verticale)
|
|
||||||
5. Apply : `aoRawTexture_` × `voxelRT_` → modulation finale (ou debug AO grayscale si debugMode=2)
|
|
||||||
- **Push constants** : width, height, normalBias, shadowMaxDist, debugMode, aoRadius, aoRayCount, aoStrength, frameIndex, historyValid
|
|
||||||
|
|
||||||
#### Phase 6.4 - Fallback [A FAIRE]
|
|
||||||
|
|
||||||
- Shadow maps + SSAO when RT not available
|
|
||||||
- `CheckCapability(RAYTRACING)` gating
|
|
||||||
|
|
||||||
### Phase 7 - Stylized Lighting (Wonderbox-inspired) [EN COURS]
|
|
||||||
|
|
||||||
#### Phase 7.1 - Hemisphere Ambient + Colored Shadows + Rim Light + Tone Mapping [FAIT]
|
|
||||||
|
|
||||||
- **Hemisphere ambient** : `lerp(groundAmbient, skyAmbient, N.y * 0.5 + 0.5)` — warm brown below, cool blue above. Applied in all 3 pixel shaders (voxelPS, voxelSmoothPS, voxelTopingPS). Vegetation gets 1.5× ambient for inter-reflection
|
|
||||||
- **Colored shadows** : RT shadows lerp toward `shadowTint` (blue-violet) instead of just darkening. `shadowFactor=0.55` (softer than 0.3)
|
|
||||||
- **Rim light** : `pow(1 - NdotV, exponent) * intensity * rimColor`. Warm golden rim on silhouettes. Reduced to 30% on vegetation (thin geometry causes halos)
|
|
||||||
- **Tone mapping + saturation** : soft exponential tone mapping (`1 - exp(-c)`) + saturation boost in `voxelAOApplyCS.hlsl` (final post-process pass)
|
|
||||||
- **CB paramètres** : `skyAmbient`, `groundAmbient`, `shadowTint`, `fogColor`, `fogParams`, `rimColor`, `rimParams`, `toneMapParams` added to VoxelCB
|
|
||||||
- **Fog centralisé** : fog density + color from CB instead of hardcoded per-shader
|
|
||||||
- **Screenshot mode** : CLI argument `screenshot` → fixed camera, 60 frames AO convergence, save `bvle_screenshot.png`, quit. Small non-intrusive window (`SW_SHOWNOACTIVATE`). No HUD.
|
|
||||||
|
|
||||||
## Métriques cibles et résultats
|
## Métriques cibles et résultats
|
||||||
|
|
||||||
| Métrique | Cible | Résultat (Ryzen 7 9800X3D + RX 9070 XT) |
|
| Métrique | Cible | Résultat (Ryzen 7 9800X3D + RX 9070 XT) |
|
||||||
|----------|-------|---------------------------------------|
|
|----------|-------|---------------------------------------|
|
||||||
| FPS 1440p | > 60 fps | ✅ 80-110 FPS (anim blocky), 700+ FPS (statique) |
|
| FPS 1440p | > 60 fps | 80-110 FPS (anim blocky), 700+ FPS (statique) |
|
||||||
| FPS anim smooth+topings | > 15 fps | ✅ 17 FPS (smooth+topings+blocky anim 60Hz) |
|
| FPS anim smooth+topings | > 15 fps | 17 FPS (smooth+topings+blocky anim 60Hz) |
|
||||||
| Meshing GPU (blocky) | < 200 µs/chunk | ✅ ~0.6 µs/chunk (0.1ms / 171 chunks) |
|
| Meshing GPU (blocky) | < 200 us/chunk | ~0.6 us/chunk (0.1ms / 171 chunks) |
|
||||||
| Meshing CPU (smooth) | < 30ms | ✅ 17ms (parallèle, 648 chunks) |
|
| Meshing CPU (smooth) | < 30ms | 17ms (parallele, 648 chunks) |
|
||||||
| Re-mesh complet | < 16ms | ✅ ~13ms blocky (regen 8.7ms + upload 4.5ms) |
|
| Memoire GPU | < 500 Mo | ~30 Mo |
|
||||||
| Mémoire GPU | < 500 Mo | ✅ ~30 Mo (11 MB voxels + 16 MB quads + buffers) |
|
| Draw calls | < 100 | 1 (GPU mesh) ou 1 (MDI) |
|
||||||
| RT shadows + AO | < 4ms en 1440p | ⏳ Phase 6 |
|
|
||||||
| Draw calls | < 100 | ✅ 1 (GPU mesh) ou 1 (MDI) |
|
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|
|
||||||
228
TROUBLESHOOTING.md
Normal file
228
TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -1013,9 +1013,11 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
uint32_t blockyVertCount = rtBlockyVertexCount_;
|
uint32_t blockyVertCount = rtBlockyVertexCount_;
|
||||||
if (blockyVertCount < 3) blockyVertCount = 0; // Need at least 1 triangle
|
if (blockyVertCount < 3) blockyVertCount = 0; // Need at least 1 triangle
|
||||||
if (blockyVertCount > 0 && blasPositionBuffer_.IsValid()) {
|
if (blockyVertCount > 0 && blasPositionBuffer_.IsValid()) {
|
||||||
// (Re)create BLAS if needed (vertex count changed or first time)
|
// Only (re)create BLAS when vertex count exceeds allocated capacity.
|
||||||
if (!blockyBLAS_.IsValid() || blockyBLAS_.desc.bottom_level.geometries.empty() ||
|
// CreateRaytracingAccelerationStructure allocates GPU memory — calling it per-frame leaks VRAM.
|
||||||
blockyBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count != blockyVertCount) {
|
// We allocate with headroom and update desc.vertex_count before each Build.
|
||||||
|
if (!blockyBLAS_.IsValid() || blockyVertCount > blockyBLASCapacity_) {
|
||||||
|
blockyBLASCapacity_ = blockyVertCount + blockyVertCount / 4; // 25% headroom
|
||||||
|
|
||||||
RaytracingAccelerationStructureDesc desc;
|
RaytracingAccelerationStructureDesc desc;
|
||||||
desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
|
desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
|
||||||
|
|
@ -1027,14 +1029,11 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
|
geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
|
||||||
geom.triangles.vertex_buffer = blasPositionBuffer_;
|
geom.triangles.vertex_buffer = blasPositionBuffer_;
|
||||||
geom.triangles.vertex_byte_offset = 0;
|
geom.triangles.vertex_byte_offset = 0;
|
||||||
geom.triangles.vertex_count = blockyVertCount;
|
geom.triangles.vertex_count = blockyBLASCapacity_; // allocate for capacity
|
||||||
geom.triangles.vertex_stride = sizeof(float) * 3; // 12 bytes per float3
|
geom.triangles.vertex_stride = sizeof(float) * 3;
|
||||||
geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
|
geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
|
||||||
// Wicked ALWAYS accesses index_buffer via to_internal() — a default GPUBuffer
|
|
||||||
// causes null deref. And DX12 treats non-zero IndexBuffer + IndexCount=0 as
|
|
||||||
// "indexed with 0 triangles" → empty BLAS. Solution: real sequential index buffer.
|
|
||||||
geom.triangles.index_buffer = blasIndexBuffer_;
|
geom.triangles.index_buffer = blasIndexBuffer_;
|
||||||
geom.triangles.index_count = blockyVertCount;
|
geom.triangles.index_count = blockyBLASCapacity_;
|
||||||
geom.triangles.index_format = IndexBufferFormat::UINT32;
|
geom.triangles.index_format = IndexBufferFormat::UINT32;
|
||||||
geom.triangles.index_offset = 0;
|
geom.triangles.index_offset = 0;
|
||||||
|
|
||||||
|
|
@ -1042,8 +1041,8 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
&blockyBLAS_);
|
&blockyBLAS_);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
dev->SetName(&blockyBLAS_, "VoxelRenderer::blockyBLAS");
|
dev->SetName(&blockyBLAS_, "VoxelRenderer::blockyBLAS");
|
||||||
wi::backlog::post("VoxelRenderer: blocky BLAS created ("
|
wi::backlog::post("VoxelRenderer: blocky BLAS created (capacity "
|
||||||
+ std::to_string(blockyVertCount / 3) + " tris)");
|
+ std::to_string(blockyBLASCapacity_ / 3) + " tris)");
|
||||||
} else {
|
} else {
|
||||||
wi::backlog::post("VoxelRenderer: failed to create blocky BLAS", wi::backlog::LogLevel::Error);
|
wi::backlog::post("VoxelRenderer: failed to create blocky BLAS", wi::backlog::LogLevel::Error);
|
||||||
rtAvailable_ = false;
|
rtAvailable_ = false;
|
||||||
|
|
@ -1051,7 +1050,9 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build BLAS
|
// Update actual vertex count in descriptor before Build
|
||||||
|
blockyBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count = blockyVertCount;
|
||||||
|
blockyBLAS_.desc.bottom_level.geometries[0].triangles.index_count = blockyVertCount;
|
||||||
dev->BuildRaytracingAccelerationStructure(&blockyBLAS_, cmd, nullptr);
|
dev->BuildRaytracingAccelerationStructure(&blockyBLAS_, cmd, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1063,8 +1064,9 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
const GPUBuffer& smoothVB = useGpuSmooth ? gpuSmoothVertexBuffer_ : smoothVertexBuffer_;
|
const GPUBuffer& smoothVB = useGpuSmooth ? gpuSmoothVertexBuffer_ : smoothVertexBuffer_;
|
||||||
|
|
||||||
if (smoothVertCount > 0 && smoothVB.IsValid()) {
|
if (smoothVertCount > 0 && smoothVB.IsValid()) {
|
||||||
if (!smoothBLAS_.IsValid() || smoothBLAS_.desc.bottom_level.geometries.empty() ||
|
// Capacity-based: only recreate when exceeding allocated capacity
|
||||||
smoothBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count != smoothVertCount) {
|
if (!smoothBLAS_.IsValid() || smoothVertCount > smoothBLASCapacity_) {
|
||||||
|
smoothBLASCapacity_ = smoothVertCount + smoothVertCount / 4; // 25% headroom
|
||||||
|
|
||||||
RaytracingAccelerationStructureDesc desc;
|
RaytracingAccelerationStructureDesc desc;
|
||||||
desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
|
desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
|
||||||
|
|
@ -1076,11 +1078,10 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
|
geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
|
||||||
geom.triangles.vertex_buffer = smoothVB;
|
geom.triangles.vertex_buffer = smoothVB;
|
||||||
geom.triangles.vertex_byte_offset = 0;
|
geom.triangles.vertex_byte_offset = 0;
|
||||||
geom.triangles.vertex_count = smoothVertCount;
|
geom.triangles.vertex_count = smoothBLASCapacity_;
|
||||||
geom.triangles.vertex_stride = 32; // SmoothVtx struct = 32 bytes, position at offset 0
|
geom.triangles.vertex_stride = 32; // SmoothVtx struct = 32 bytes, position at offset 0
|
||||||
// Wicked always accesses index_buffer — must be valid + use real indices
|
|
||||||
geom.triangles.index_buffer = blasIndexBuffer_;
|
geom.triangles.index_buffer = blasIndexBuffer_;
|
||||||
geom.triangles.index_count = smoothVertCount;
|
geom.triangles.index_count = smoothBLASCapacity_;
|
||||||
geom.triangles.index_format = IndexBufferFormat::UINT32;
|
geom.triangles.index_format = IndexBufferFormat::UINT32;
|
||||||
geom.triangles.index_offset = 0;
|
geom.triangles.index_offset = 0;
|
||||||
geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
|
geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
|
||||||
|
|
@ -1089,14 +1090,17 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
&smoothBLAS_);
|
&smoothBLAS_);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
dev->SetName(&smoothBLAS_, "VoxelRenderer::smoothBLAS");
|
dev->SetName(&smoothBLAS_, "VoxelRenderer::smoothBLAS");
|
||||||
wi::backlog::post("VoxelRenderer: smooth BLAS created ("
|
wi::backlog::post("VoxelRenderer: smooth BLAS created (capacity "
|
||||||
+ std::to_string(smoothVertCount / 3) + " tris)");
|
+ std::to_string(smoothBLASCapacity_ / 3) + " tris)");
|
||||||
} else {
|
} else {
|
||||||
wi::backlog::post("VoxelRenderer: failed to create smooth BLAS", wi::backlog::LogLevel::Error);
|
wi::backlog::post("VoxelRenderer: failed to create smooth BLAS", wi::backlog::LogLevel::Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smoothBLAS_.IsValid()) {
|
if (smoothBLAS_.IsValid()) {
|
||||||
|
// Update actual vertex count before Build
|
||||||
|
smoothBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count = smoothVertCount;
|
||||||
|
smoothBLAS_.desc.bottom_level.geometries[0].triangles.index_count = smoothVertCount;
|
||||||
dev->BuildRaytracingAccelerationStructure(&smoothBLAS_, cmd, nullptr);
|
dev->BuildRaytracingAccelerationStructure(&smoothBLAS_, cmd, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1106,12 +1110,13 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
// ── Toping BLAS ──────────────────────────────────────────────
|
// ── Toping BLAS ──────────────────────────────────────────────
|
||||||
uint32_t topingVertCount = rtTopingVertexCount_;
|
uint32_t topingVertCount = rtTopingVertexCount_;
|
||||||
if (topingVertCount >= 3 && topingBLASPositionBuffer_.IsValid()) {
|
if (topingVertCount >= 3 && topingBLASPositionBuffer_.IsValid()) {
|
||||||
if (!topingBLAS_.IsValid() || topingBLAS_.desc.bottom_level.geometries.empty() ||
|
// Capacity-based: only recreate when exceeding allocated capacity
|
||||||
topingBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count != topingVertCount) {
|
if (!topingBLAS_.IsValid() || topingVertCount > topingBLASASCapacity_) {
|
||||||
|
topingBLASASCapacity_ = topingVertCount + topingVertCount / 4; // 25% headroom
|
||||||
|
|
||||||
RaytracingAccelerationStructureDesc desc;
|
RaytracingAccelerationStructureDesc desc;
|
||||||
desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
|
desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
|
||||||
desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_TRACE; // optimize traversal
|
desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_BUILD; // fast rebuild for animation
|
||||||
|
|
||||||
desc.bottom_level.geometries.resize(1);
|
desc.bottom_level.geometries.resize(1);
|
||||||
auto& geom = desc.bottom_level.geometries[0];
|
auto& geom = desc.bottom_level.geometries[0];
|
||||||
|
|
@ -1119,25 +1124,28 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
|
geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
|
||||||
geom.triangles.vertex_buffer = topingBLASPositionBuffer_;
|
geom.triangles.vertex_buffer = topingBLASPositionBuffer_;
|
||||||
geom.triangles.vertex_byte_offset = 0;
|
geom.triangles.vertex_byte_offset = 0;
|
||||||
geom.triangles.vertex_count = topingVertCount;
|
geom.triangles.vertex_count = topingBLASASCapacity_;
|
||||||
geom.triangles.vertex_stride = sizeof(float) * 3;
|
geom.triangles.vertex_stride = sizeof(float) * 3;
|
||||||
geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
|
geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
|
||||||
geom.triangles.index_buffer = topingBLASIndexBuffer_;
|
geom.triangles.index_buffer = topingBLASIndexBuffer_;
|
||||||
geom.triangles.index_count = topingVertCount;
|
geom.triangles.index_count = topingBLASASCapacity_;
|
||||||
geom.triangles.index_format = IndexBufferFormat::UINT32;
|
geom.triangles.index_format = IndexBufferFormat::UINT32;
|
||||||
geom.triangles.index_offset = 0;
|
geom.triangles.index_offset = 0;
|
||||||
|
|
||||||
bool ok = dev->CreateRaytracingAccelerationStructure(&desc, &topingBLAS_);
|
bool ok = dev->CreateRaytracingAccelerationStructure(&desc, &topingBLAS_);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
dev->SetName(&topingBLAS_, "VoxelRenderer::topingBLAS");
|
dev->SetName(&topingBLAS_, "VoxelRenderer::topingBLAS");
|
||||||
wi::backlog::post("VoxelRenderer: toping BLAS created ("
|
wi::backlog::post("VoxelRenderer: toping BLAS created (capacity "
|
||||||
+ std::to_string(topingVertCount / 3) + " tris)");
|
+ std::to_string(topingBLASASCapacity_ / 3) + " tris)");
|
||||||
} else {
|
} else {
|
||||||
wi::backlog::post("VoxelRenderer: failed to create toping BLAS", wi::backlog::LogLevel::Error);
|
wi::backlog::post("VoxelRenderer: failed to create toping BLAS", wi::backlog::LogLevel::Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topingBLAS_.IsValid()) {
|
if (topingBLAS_.IsValid()) {
|
||||||
|
// Update actual vertex count before Build
|
||||||
|
topingBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count = topingVertCount;
|
||||||
|
topingBLAS_.desc.bottom_level.geometries[0].triangles.index_count = topingVertCount;
|
||||||
dev->BuildRaytracingAccelerationStructure(&topingBLAS_, cmd, nullptr);
|
dev->BuildRaytracingAccelerationStructure(&topingBLAS_, cmd, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1151,18 +1159,19 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TLAS (up to 3 instances: blocky + smooth + topings) ────
|
// ── TLAS (up to 3 instances: blocky + smooth + topings) ────
|
||||||
// Always recreate TLAS with pre-filled instance data via CreateBuffer2.
|
// Only recreate TLAS when instance count changes. BLASes are capacity-based
|
||||||
// RAY_TRACING instance buffers have special resource state requirements,
|
// (never recreated during animation), so BLAS pointers remain stable.
|
||||||
// so UpdateBuffer (CopyBufferRegion) would crash on state mismatch.
|
// For subsequent frames, just rebuild the TLAS to pick up rebuilt BLASes.
|
||||||
uint32_t instanceCount = 0;
|
uint32_t instanceCount = 0;
|
||||||
if (blockyBLAS_.IsValid()) instanceCount++;
|
if (blockyBLAS_.IsValid()) instanceCount++;
|
||||||
if (smoothBLAS_.IsValid() && smoothVertCount > 0) instanceCount++;
|
if (smoothBLAS_.IsValid() && smoothVertCount > 0) instanceCount++;
|
||||||
if (topingBLAS_.IsValid() && topingVertCount >= 3) instanceCount++;
|
if (topingBLAS_.IsValid() && topingVertCount >= 3) instanceCount++;
|
||||||
if (instanceCount == 0) { rtDirty_ = false; return; }
|
if (instanceCount == 0) { rtDirty_ = false; return; }
|
||||||
|
|
||||||
|
// Recreate TLAS only when instance count changes (avoids per-frame GPU allocation)
|
||||||
|
if (!tlas_.IsValid() || instanceCount != tlasInstanceCount_) {
|
||||||
const size_t instSize = dev->GetTopLevelAccelerationStructureInstanceSize();
|
const size_t instSize = dev->GetTopLevelAccelerationStructureInstanceSize();
|
||||||
|
|
||||||
// Identity transform (3x4 row-major)
|
|
||||||
auto setIdentity = [](float transform[3][4]) {
|
auto setIdentity = [](float transform[3][4]) {
|
||||||
std::memset(transform, 0, sizeof(float) * 12);
|
std::memset(transform, 0, sizeof(float) * 12);
|
||||||
transform[0][0] = 1.0f;
|
transform[0][0] = 1.0f;
|
||||||
|
|
@ -1170,13 +1179,10 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
transform[2][2] = 1.0f;
|
transform[2][2] = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Capture BLAS pointers for the lambda (can't capture member references)
|
|
||||||
const RaytracingAccelerationStructure* blockyBLASPtr = blockyBLAS_.IsValid() ? &blockyBLAS_ : nullptr;
|
const RaytracingAccelerationStructure* blockyBLASPtr = blockyBLAS_.IsValid() ? &blockyBLAS_ : nullptr;
|
||||||
const RaytracingAccelerationStructure* smoothBLASPtr = (smoothBLAS_.IsValid() && smoothVertCount > 0) ? &smoothBLAS_ : nullptr;
|
const RaytracingAccelerationStructure* smoothBLASPtr = (smoothBLAS_.IsValid() && smoothVertCount > 0) ? &smoothBLAS_ : nullptr;
|
||||||
const RaytracingAccelerationStructure* topingBLASPtr = (topingBLAS_.IsValid() && topingVertCount >= 3) ? &topingBLAS_ : nullptr;
|
const RaytracingAccelerationStructure* topingBLASPtr = (topingBLAS_.IsValid() && topingVertCount >= 3) ? &topingBLAS_ : nullptr;
|
||||||
|
|
||||||
// Create TLAS with instance data pre-filled in the creation callback.
|
|
||||||
// This avoids any UpdateBuffer on RAY_TRACING flagged buffers.
|
|
||||||
RaytracingAccelerationStructureDesc desc;
|
RaytracingAccelerationStructureDesc desc;
|
||||||
desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_BUILD;
|
desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_BUILD;
|
||||||
desc.type = RaytracingAccelerationStructureDesc::Type::TOPLEVEL;
|
desc.type = RaytracingAccelerationStructureDesc::Type::TOPLEVEL;
|
||||||
|
|
@ -1189,38 +1195,29 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
|
|
||||||
auto initInstances = [&](void* dest) {
|
auto initInstances = [&](void* dest) {
|
||||||
uint32_t idx = 0;
|
uint32_t idx = 0;
|
||||||
|
|
||||||
if (blockyBLASPtr) {
|
if (blockyBLASPtr) {
|
||||||
RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
|
RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
|
||||||
setIdentity(inst.transform);
|
setIdentity(inst.transform);
|
||||||
inst.instance_id = 0;
|
inst.instance_id = 0; inst.instance_mask = 0xFF;
|
||||||
inst.instance_mask = 0xFF;
|
inst.instance_contribution_to_hit_group_index = 0; inst.flags = 0;
|
||||||
inst.instance_contribution_to_hit_group_index = 0;
|
|
||||||
inst.flags = 0;
|
|
||||||
inst.bottom_level = blockyBLASPtr;
|
inst.bottom_level = blockyBLASPtr;
|
||||||
dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
|
dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smoothBLASPtr) {
|
if (smoothBLASPtr) {
|
||||||
RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
|
RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
|
||||||
setIdentity(inst.transform);
|
setIdentity(inst.transform);
|
||||||
inst.instance_id = 1;
|
inst.instance_id = 1; inst.instance_mask = 0xFF;
|
||||||
inst.instance_mask = 0xFF;
|
inst.instance_contribution_to_hit_group_index = 0; inst.flags = 0;
|
||||||
inst.instance_contribution_to_hit_group_index = 0;
|
|
||||||
inst.flags = 0;
|
|
||||||
inst.bottom_level = smoothBLASPtr;
|
inst.bottom_level = smoothBLASPtr;
|
||||||
dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
|
dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topingBLASPtr) {
|
if (topingBLASPtr) {
|
||||||
RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
|
RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
|
||||||
setIdentity(inst.transform);
|
setIdentity(inst.transform);
|
||||||
inst.instance_id = 2;
|
inst.instance_id = 2; inst.instance_mask = 0xFF;
|
||||||
inst.instance_mask = 0xFF;
|
inst.instance_contribution_to_hit_group_index = 0; inst.flags = 0;
|
||||||
inst.instance_contribution_to_hit_group_index = 0;
|
|
||||||
inst.flags = 0;
|
|
||||||
inst.bottom_level = topingBLASPtr;
|
inst.bottom_level = topingBLASPtr;
|
||||||
dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
|
dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
|
||||||
idx++;
|
idx++;
|
||||||
|
|
@ -1234,15 +1231,18 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = dev->CreateRaytracingAccelerationStructure(&desc,
|
ok = dev->CreateRaytracingAccelerationStructure(&desc, &tlas_);
|
||||||
&tlas_);
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
wi::backlog::post("VoxelRenderer: failed to create TLAS", wi::backlog::LogLevel::Error);
|
wi::backlog::post("VoxelRenderer: failed to create TLAS", wi::backlog::LogLevel::Error);
|
||||||
rtDirty_ = false;
|
rtDirty_ = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build TLAS
|
tlasInstanceCount_ = instanceCount;
|
||||||
|
wi::backlog::post("VoxelRenderer: TLAS created (" + std::to_string(instanceCount) + " instances)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild TLAS (picks up rebuilt BLASes with new vertex data)
|
||||||
dev->BuildRaytracingAccelerationStructure(&tlas_, cmd, nullptr);
|
dev->BuildRaytracingAccelerationStructure(&tlas_, cmd, nullptr);
|
||||||
|
|
||||||
// Memory barrier: sync TLAS build before ray queries can use it
|
// Memory barrier: sync TLAS build before ray queries can use it
|
||||||
|
|
@ -1408,6 +1408,7 @@ void VoxelRenderer::dispatchShadows(CommandList cmd,
|
||||||
|
|
||||||
dev->BindComputeShader(&aoApplyShader_, cmd);
|
dev->BindComputeShader(&aoApplyShader_, cmd);
|
||||||
dev->BindResource(&aoRawTexture_, 0, cmd); // t0 = blurred AO
|
dev->BindResource(&aoRawTexture_, 0, cmd); // t0 = blurred AO
|
||||||
|
dev->BindResource(&depthBuffer, 1, cmd); // t1 = depth (for sky detection)
|
||||||
dev->BindUAV(&renderTarget, 0, cmd); // u0 = color
|
dev->BindUAV(&renderTarget, 0, cmd); // u0 = color
|
||||||
|
|
||||||
struct ApplyPush {
|
struct ApplyPush {
|
||||||
|
|
@ -2037,18 +2038,33 @@ void VoxelRenderer::uploadTopingData(const TopingSystem& topingSystem) {
|
||||||
topingGpuInsts_[i] = { topingSorted_[i].wx, topingSorted_[i].wy, topingSorted_[i].wz };
|
topingGpuInsts_[i] = { topingSorted_[i].wx, topingSorted_[i].wy, topingSorted_[i].wz };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recreate buffer each frame (UpdateBuffer requires barrier management).
|
// Pre-allocate instance buffer; only recreate when capacity needs to grow.
|
||||||
// Persistent staging vectors eliminate per-frame heap allocations.
|
// Data upload deferred to Render() via UpdateBuffer (needs CommandList).
|
||||||
|
if (!topingInstanceBuffer_.IsValid() || topingInstanceCapacity_ < instCount) {
|
||||||
|
topingInstanceCapacity_ = instCount + instCount / 4; // 25% headroom
|
||||||
GPUBufferDesc ibDesc;
|
GPUBufferDesc ibDesc;
|
||||||
ibDesc.size = instCount * sizeof(TopingGPUInst);
|
ibDesc.size = topingInstanceCapacity_ * sizeof(TopingGPUInst);
|
||||||
ibDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
ibDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
ibDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
ibDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
ibDesc.stride = sizeof(TopingGPUInst);
|
ibDesc.stride = sizeof(TopingGPUInst);
|
||||||
ibDesc.usage = Usage::DEFAULT;
|
ibDesc.usage = Usage::DEFAULT;
|
||||||
device_->CreateBuffer(&ibDesc, topingGpuInsts_.data(), &topingInstanceBuffer_);
|
// Create WITHOUT data — Wicked copies desc.size bytes from data ptr,
|
||||||
|
// which would overread our vector (instCount < topingInstanceCapacity_).
|
||||||
|
// Actual data upload deferred to Render() via UpdateBuffer.
|
||||||
|
device_->CreateBuffer(&ibDesc, nullptr, &topingInstanceBuffer_);
|
||||||
|
topingInstanceDirty_ = true; // upload data in Render()
|
||||||
|
char msg[128];
|
||||||
|
snprintf(msg, sizeof(msg), "Toping: allocated instance buffer (%u capacity, %.1f KB)",
|
||||||
|
topingInstanceCapacity_, topingInstanceCapacity_ * sizeof(TopingGPUInst) / 1024.0f);
|
||||||
|
wi::backlog::post(msg);
|
||||||
|
} else {
|
||||||
|
topingInstanceDirty_ = true; // deferred upload in Render()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Build toping BLAS position buffer ───────────────────────
|
// ── Build toping BLAS position data ────────────────────────
|
||||||
// Expand (mesh vertices × instances) → world-space float3 positions.
|
// Compute world-space positions for all toping instances.
|
||||||
|
// Buffer is pre-allocated once (no per-frame CreateBuffer), data uploaded
|
||||||
|
// in Render() via UpdateBuffer (deferred — needs CommandList).
|
||||||
const auto& defs = topingSystem.getDefs();
|
const auto& defs = topingSystem.getDefs();
|
||||||
uint32_t totalTopingVerts = 0;
|
uint32_t totalTopingVerts = 0;
|
||||||
for (uint32_t i = 0; i < instCount; i++) {
|
for (uint32_t i = 0; i < instCount; i++) {
|
||||||
|
|
@ -2058,50 +2074,60 @@ void VoxelRenderer::uploadTopingData(const TopingSystem& topingSystem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalTopingVerts > 0 && !verts.empty()) {
|
if (totalTopingVerts > 0 && !verts.empty()) {
|
||||||
std::vector<float> positions(totalTopingVerts * 3); // float3 per vertex
|
// Fill staging buffer with world-space positions
|
||||||
|
topingBLASPositionStaging_.resize(totalTopingVerts * 3);
|
||||||
uint32_t outIdx = 0;
|
uint32_t outIdx = 0;
|
||||||
for (uint32_t i = 0; i < instCount; i++) {
|
for (uint32_t i = 0; i < instCount; i++) {
|
||||||
const auto& si = topingSorted_[i];
|
const auto& si = topingSorted_[i];
|
||||||
if (si.type >= defs.size()) continue;
|
if (si.type >= defs.size()) continue;
|
||||||
|
if (si.variant >= 16) continue;
|
||||||
const auto& slice = defs[si.type].variants[si.variant];
|
const auto& slice = defs[si.type].variants[si.variant];
|
||||||
for (uint32_t v = 0; v < slice.count; v++) {
|
for (uint32_t v = 0; v < slice.count; v++) {
|
||||||
|
if (slice.offset + v >= verts.size()) break;
|
||||||
|
if (outIdx >= totalTopingVerts) break;
|
||||||
const auto& vtx = verts[slice.offset + v];
|
const auto& vtx = verts[slice.offset + v];
|
||||||
positions[outIdx * 3 + 0] = vtx.px + si.wx;
|
topingBLASPositionStaging_[outIdx * 3 + 0] = vtx.px + si.wx;
|
||||||
positions[outIdx * 3 + 1] = vtx.py + si.wy;
|
topingBLASPositionStaging_[outIdx * 3 + 1] = vtx.py + si.wy;
|
||||||
positions[outIdx * 3 + 2] = vtx.pz + si.wz;
|
topingBLASPositionStaging_[outIdx * 3 + 2] = vtx.pz + si.wz;
|
||||||
outIdx++;
|
outIdx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
topingBLASVertexCount_ = outIdx;
|
||||||
|
|
||||||
// Create position buffer
|
// Pre-allocate GPU buffer once; grow only when needed.
|
||||||
|
// No RAY_TRACING flag — BLAS vertex buffers work with SHADER_RESOURCE
|
||||||
|
// (same pattern as blocky blasPositionBuffer_). This allows UpdateBuffer.
|
||||||
|
if (!topingBLASPositionBuffer_.IsValid() || topingBLASPositionCapacity_ < outIdx) {
|
||||||
|
topingBLASPositionCapacity_ = outIdx + outIdx / 4; // 25% headroom
|
||||||
GPUBufferDesc posDesc;
|
GPUBufferDesc posDesc;
|
||||||
posDesc.size = totalTopingVerts * sizeof(float) * 3;
|
posDesc.size = (size_t)topingBLASPositionCapacity_ * 3 * sizeof(float);
|
||||||
posDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
posDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
posDesc.misc_flags = ResourceMiscFlag::RAY_TRACING;
|
posDesc.misc_flags = ResourceMiscFlag::NONE;
|
||||||
posDesc.usage = Usage::DEFAULT;
|
posDesc.usage = Usage::DEFAULT;
|
||||||
device_->CreateBuffer(&posDesc, positions.data(), &topingBLASPositionBuffer_);
|
device_->CreateBuffer(&posDesc, nullptr, &topingBLASPositionBuffer_);
|
||||||
|
|
||||||
// Create sequential index buffer (Wicked requires valid index buffer for BLAS)
|
char msg[256];
|
||||||
if (topingBLASIndexCount_ < totalTopingVerts) {
|
snprintf(msg, sizeof(msg), "Toping BLAS: allocated pos buffer (%u capacity, %.1f MB)",
|
||||||
std::vector<uint32_t> indices(totalTopingVerts);
|
topingBLASPositionCapacity_, posDesc.size / (1024.0 * 1024.0));
|
||||||
for (uint32_t j = 0; j < totalTopingVerts; j++) indices[j] = j;
|
wi::backlog::post(msg);
|
||||||
|
|
||||||
GPUBufferDesc idxDesc;
|
|
||||||
idxDesc.size = totalTopingVerts * sizeof(uint32_t);
|
|
||||||
idxDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
|
||||||
idxDesc.misc_flags = ResourceMiscFlag::RAY_TRACING;
|
|
||||||
idxDesc.usage = Usage::DEFAULT;
|
|
||||||
device_->CreateBuffer(&idxDesc, indices.data(), &topingBLASIndexBuffer_);
|
|
||||||
topingBLASIndexCount_ = totalTopingVerts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rtTopingVertexCount_ = totalTopingVerts;
|
// Pre-allocate index buffer (sequential [0,1,2,...]) — grow only when needed
|
||||||
rtDirty_ = true;
|
if (topingBLASIndexCount_ < topingBLASPositionCapacity_) {
|
||||||
|
uint32_t idxCount = topingBLASPositionCapacity_;
|
||||||
|
std::vector<uint32_t> indices(idxCount);
|
||||||
|
for (uint32_t j = 0; j < idxCount; j++) indices[j] = j;
|
||||||
|
|
||||||
char msg[128];
|
GPUBufferDesc idxDesc;
|
||||||
snprintf(msg, sizeof(msg), "Toping BLAS: %u vertices (%u tris)",
|
idxDesc.size = (size_t)idxCount * sizeof(uint32_t);
|
||||||
totalTopingVerts, totalTopingVerts / 3);
|
idxDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
wi::backlog::post(msg);
|
idxDesc.misc_flags = ResourceMiscFlag::NONE;
|
||||||
|
idxDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&idxDesc, indices.data(), &topingBLASIndexBuffer_);
|
||||||
|
topingBLASIndexCount_ = idxCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
topingBLASDirty_ = true; // deferred upload + BLAS rebuild in Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2257,15 +2283,23 @@ void VoxelRenderer::uploadSmoothData(VoxelWorld& world) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recreate buffer each frame (UpdateBuffer requires barrier management).
|
// Pre-allocate smooth buffer; only recreate when capacity needs to grow.
|
||||||
// Persistent staging vector eliminates per-frame heap allocations.
|
if (!smoothVertexBuffer_.IsValid() || smoothVertexCapacity_ < smoothVertexCount_) {
|
||||||
|
smoothVertexCapacity_ = smoothVertexCount_ + smoothVertexCount_ / 4; // 25% headroom
|
||||||
GPUBufferDesc vbDesc;
|
GPUBufferDesc vbDesc;
|
||||||
vbDesc.size = smoothVertexCount_ * sizeof(SmoothVertex);
|
vbDesc.size = smoothVertexCapacity_ * sizeof(SmoothVertex);
|
||||||
vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
vbDesc.stride = sizeof(SmoothVertex);
|
vbDesc.stride = sizeof(SmoothVertex);
|
||||||
vbDesc.usage = Usage::DEFAULT;
|
vbDesc.usage = Usage::DEFAULT;
|
||||||
device_->CreateBuffer(&vbDesc, smoothStagingVerts_.data(), &smoothVertexBuffer_);
|
// Create WITHOUT data — capacity > vertex count, Wicked would overread.
|
||||||
|
device_->CreateBuffer(&vbDesc, nullptr, &smoothVertexBuffer_);
|
||||||
|
smoothVertexDirty_ = true; // upload data in Render()
|
||||||
|
wi::backlog::post("Smooth: allocated vertex buffer (" + std::to_string(smoothVertexCapacity_)
|
||||||
|
+ " capacity, " + std::to_string(smoothVertexCapacity_ * sizeof(SmoothVertex) / 1024) + " KB)");
|
||||||
|
} else {
|
||||||
|
smoothVertexDirty_ = true; // deferred upload in Render()
|
||||||
|
}
|
||||||
|
|
||||||
smoothDirty_ = false;
|
smoothDirty_ = false;
|
||||||
}
|
}
|
||||||
|
|
@ -2294,13 +2328,21 @@ void VoxelRenderer::uploadSmoothDataFast(VoxelWorld& world) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-allocate smooth buffer; only recreate when capacity needs to grow.
|
||||||
|
if (!smoothVertexBuffer_.IsValid() || smoothVertexCapacity_ < smoothVertexCount_) {
|
||||||
|
smoothVertexCapacity_ = smoothVertexCount_ + smoothVertexCount_ / 4;
|
||||||
GPUBufferDesc vbDesc;
|
GPUBufferDesc vbDesc;
|
||||||
vbDesc.size = smoothVertexCount_ * sizeof(SmoothVertex);
|
vbDesc.size = smoothVertexCapacity_ * sizeof(SmoothVertex);
|
||||||
vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
vbDesc.stride = sizeof(SmoothVertex);
|
vbDesc.stride = sizeof(SmoothVertex);
|
||||||
vbDesc.usage = Usage::DEFAULT;
|
vbDesc.usage = Usage::DEFAULT;
|
||||||
device_->CreateBuffer(&vbDesc, smoothStagingVerts_.data(), &smoothVertexBuffer_);
|
// Create WITHOUT data — capacity > vertex count, Wicked would overread.
|
||||||
|
device_->CreateBuffer(&vbDesc, nullptr, &smoothVertexBuffer_);
|
||||||
|
smoothVertexDirty_ = true; // upload data in Render()
|
||||||
|
} else {
|
||||||
|
smoothVertexDirty_ = true; // deferred upload in Render()
|
||||||
|
}
|
||||||
|
|
||||||
smoothDirty_ = false;
|
smoothDirty_ = false;
|
||||||
}
|
}
|
||||||
|
|
@ -2403,7 +2445,6 @@ void VoxelRenderPath::Start() {
|
||||||
} else {
|
} else {
|
||||||
world.generateAround(cameraPos.x, cameraPos.y, cameraPos.z, 4);
|
world.generateAround(cameraPos.x, cameraPos.y, cameraPos.z, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Screenshot mode: fixed camera with good framing of terrain
|
// Screenshot mode: fixed camera with good framing of terrain
|
||||||
if (screenshotMode) {
|
if (screenshotMode) {
|
||||||
cameraPos = { 270.0f, 50.0f, 240.0f }; // above terrain, below sky
|
cameraPos = { 270.0f, 50.0f, 240.0f }; // above terrain, below sky
|
||||||
|
|
@ -2646,6 +2687,7 @@ void VoxelRenderPath::Update(float dt) {
|
||||||
|
|
||||||
renderer.voxelCacheDirty_ = false; // cache already filled by fused pack
|
renderer.voxelCacheDirty_ = false; // cache already filled by fused pack
|
||||||
renderer.gpuMeshDirty_ = true; // GPU still needs upload + dispatch
|
renderer.gpuMeshDirty_ = true; // GPU still needs upload + dispatch
|
||||||
|
renderer.aoHistoryValid_ = false; // invalidate temporal AO (geometry changed)
|
||||||
|
|
||||||
// Re-mesh smooth surfaces — GPU path or CPU fallback
|
// Re-mesh smooth surfaces — GPU path or CPU fallback
|
||||||
if (renderer.smoothCentroidShader_.IsValid() && renderer.smoothMeshShader_.IsValid()) {
|
if (renderer.smoothCentroidShader_.IsValid() && renderer.smoothMeshShader_.IsValid()) {
|
||||||
|
|
@ -2744,6 +2786,21 @@ void VoxelRenderPath::Render() const {
|
||||||
renderer.gpuSmoothMeshDirty_ = true;
|
renderer.gpuSmoothMeshDirty_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Deferred toping BLAS position upload (must happen BEFORE BLAS build) ──
|
||||||
|
if (renderer.topingBLASDirty_ && renderer.topingBLASPositionBuffer_.IsValid() &&
|
||||||
|
renderer.topingBLASVertexCount_ > 0 &&
|
||||||
|
!renderer.topingBLASPositionStaging_.empty()) {
|
||||||
|
size_t uploadSize = (size_t)renderer.topingBLASVertexCount_ * 3 * sizeof(float);
|
||||||
|
size_t bufferSize = (size_t)renderer.topingBLASPositionCapacity_ * 3 * sizeof(float);
|
||||||
|
if (uploadSize <= bufferSize) {
|
||||||
|
device->UpdateBuffer(&renderer.topingBLASPositionBuffer_,
|
||||||
|
renderer.topingBLASPositionStaging_.data(), cmd, uploadSize);
|
||||||
|
}
|
||||||
|
renderer.rtTopingVertexCount_ = renderer.topingBLASVertexCount_;
|
||||||
|
renderer.rtDirty_ = true; // trigger BLAS/TLAS rebuild
|
||||||
|
renderer.topingBLASDirty_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 6.1: BLAS extraction + acceleration structure build
|
// Phase 6.1: BLAS extraction + acceleration structure build
|
||||||
if (renderer.rtAvailable_ && renderer.blasExtractShader_.IsValid() &&
|
if (renderer.rtAvailable_ && renderer.blasExtractShader_.IsValid() &&
|
||||||
renderer.gpuMeshQuadCount_ > 0 &&
|
renderer.gpuMeshQuadCount_ > 0 &&
|
||||||
|
|
@ -2762,6 +2819,29 @@ void VoxelRenderPath::Render() const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Deferred GPU uploads (dirty flags set in Update(), need CommandList) ──
|
||||||
|
if (renderer.topingInstanceDirty_ && renderer.topingInstanceBuffer_.IsValid() &&
|
||||||
|
!renderer.topingGpuInsts_.empty()) {
|
||||||
|
size_t uploadSize = renderer.topingGpuInsts_.size() * sizeof(VoxelRenderer::TopingGPUInst);
|
||||||
|
size_t bufferSize = renderer.topingInstanceCapacity_ * sizeof(VoxelRenderer::TopingGPUInst);
|
||||||
|
if (uploadSize <= bufferSize) {
|
||||||
|
device->UpdateBuffer(&renderer.topingInstanceBuffer_,
|
||||||
|
renderer.topingGpuInsts_.data(), cmd, uploadSize);
|
||||||
|
}
|
||||||
|
renderer.topingInstanceDirty_ = false;
|
||||||
|
}
|
||||||
|
if (renderer.smoothVertexDirty_ && renderer.smoothVertexBuffer_.IsValid() &&
|
||||||
|
renderer.smoothVertexCount_ > 0 &&
|
||||||
|
renderer.smoothVertexCount_ <= renderer.smoothStagingVerts_.size()) {
|
||||||
|
size_t uploadSize = renderer.smoothVertexCount_ * sizeof(SmoothVertex);
|
||||||
|
size_t bufferSize = renderer.smoothVertexCapacity_ * sizeof(SmoothVertex);
|
||||||
|
if (uploadSize <= bufferSize) {
|
||||||
|
device->UpdateBuffer(&renderer.smoothVertexBuffer_,
|
||||||
|
renderer.smoothStagingVerts_.data(), cmd, uploadSize);
|
||||||
|
}
|
||||||
|
renderer.smoothVertexDirty_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
auto tRender0 = std::chrono::high_resolution_clock::now();
|
auto tRender0 = std::chrono::high_resolution_clock::now();
|
||||||
renderer.render(cmd, *camera, voxelDepth_, voxelRT_, voxelNormalRT_);
|
renderer.render(cmd, *camera, voxelDepth_, voxelRT_, voxelNormalRT_);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ private:
|
||||||
wi::graphics::PipelineState topingPso_;
|
wi::graphics::PipelineState topingPso_;
|
||||||
wi::graphics::GPUBuffer topingVertexBuffer_; // StructuredBuffer<TopingVertex>, SRV t4
|
wi::graphics::GPUBuffer topingVertexBuffer_; // StructuredBuffer<TopingVertex>, SRV t4
|
||||||
wi::graphics::GPUBuffer topingInstanceBuffer_; // StructuredBuffer<float3>, SRV t5
|
wi::graphics::GPUBuffer topingInstanceBuffer_; // StructuredBuffer<float3>, SRV t5
|
||||||
|
mutable uint32_t topingInstanceCapacity_ = 0; // pre-allocated capacity (avoid per-frame CreateBuffer)
|
||||||
|
mutable bool topingInstanceDirty_ = false; // deferred upload via UpdateBuffer in Render()
|
||||||
static constexpr uint32_t MAX_TOPING_INSTANCES = 256 * 1024; // 256K instances max
|
static constexpr uint32_t MAX_TOPING_INSTANCES = 256 * 1024; // 256K instances max
|
||||||
// Persistent staging buffers for toping upload (avoids per-frame allocations)
|
// Persistent staging buffers for toping upload (avoids per-frame allocations)
|
||||||
struct TopingSortedInst { float wx, wy, wz; uint16_t type, variant; };
|
struct TopingSortedInst { float wx, wy, wz; uint16_t type, variant; };
|
||||||
|
|
@ -97,10 +99,12 @@ private:
|
||||||
wi::graphics::RasterizerState smoothRasterizer_;
|
wi::graphics::RasterizerState smoothRasterizer_;
|
||||||
wi::graphics::PipelineState smoothPso_;
|
wi::graphics::PipelineState smoothPso_;
|
||||||
wi::graphics::GPUBuffer smoothVertexBuffer_; // StructuredBuffer<SmoothVertex>, SRV t6
|
wi::graphics::GPUBuffer smoothVertexBuffer_; // StructuredBuffer<SmoothVertex>, SRV t6
|
||||||
|
mutable uint32_t smoothVertexCapacity_ = 0; // pre-allocated capacity (avoid per-frame CreateBuffer)
|
||||||
std::vector<SmoothVertex> smoothStagingVerts_; // persistent staging buffer (avoids per-frame alloc)
|
std::vector<SmoothVertex> smoothStagingVerts_; // persistent staging buffer (avoids per-frame alloc)
|
||||||
static constexpr uint32_t MAX_SMOOTH_VERTICES = 4 * 1024 * 1024; // 4M vertices max
|
static constexpr uint32_t MAX_SMOOTH_VERTICES = 4 * 1024 * 1024; // 4M vertices max
|
||||||
mutable uint32_t smoothVertexCount_ = 0;
|
mutable uint32_t smoothVertexCount_ = 0;
|
||||||
mutable uint32_t smoothDrawCalls_ = 0;
|
mutable uint32_t smoothDrawCalls_ = 0;
|
||||||
|
mutable bool smoothVertexDirty_ = false; // deferred upload via UpdateBuffer in Render()
|
||||||
bool smoothDirty_ = true;
|
bool smoothDirty_ = true;
|
||||||
|
|
||||||
// Texture array for materials (256x256, 5 layers for prototype)
|
// Texture array for materials (256x256, 5 layers for prototype)
|
||||||
|
|
@ -210,13 +214,22 @@ private:
|
||||||
mutable wi::graphics::RaytracingAccelerationStructure tlas_;
|
mutable wi::graphics::RaytracingAccelerationStructure tlas_;
|
||||||
mutable wi::graphics::GPUBuffer topingBLASPositionBuffer_; // float3[] world-space toping positions
|
mutable wi::graphics::GPUBuffer topingBLASPositionBuffer_; // float3[] world-space toping positions
|
||||||
mutable wi::graphics::GPUBuffer topingBLASIndexBuffer_; // sequential indices for toping BLAS
|
mutable wi::graphics::GPUBuffer topingBLASIndexBuffer_; // sequential indices for toping BLAS
|
||||||
|
mutable uint32_t topingBLASPositionCapacity_ = 0; // pre-allocated capacity (vertices)
|
||||||
mutable uint32_t topingBLASIndexCount_ = 0; // size of toping index buffer
|
mutable uint32_t topingBLASIndexCount_ = 0; // size of toping index buffer
|
||||||
|
mutable bool topingBLASDirty_ = false; // deferred BLAS position upload + rebuild
|
||||||
|
mutable uint32_t topingBLASVertexCount_ = 0; // actual vertex count for current frame
|
||||||
|
std::vector<float> topingBLASPositionStaging_; // CPU staging for deferred upload
|
||||||
static constexpr uint32_t MAX_BLAS_VERTICES = MEGA_BUFFER_CAPACITY * 6; // 6 verts per quad
|
static constexpr uint32_t MAX_BLAS_VERTICES = MEGA_BUFFER_CAPACITY * 6; // 6 verts per quad
|
||||||
mutable bool rtAvailable_ = false; // GPU supports RT
|
mutable bool rtAvailable_ = false; // GPU supports RT
|
||||||
mutable bool rtDirty_ = true; // BLAS/TLAS need rebuild
|
mutable bool rtDirty_ = true; // BLAS/TLAS need rebuild
|
||||||
mutable uint32_t rtBlockyVertexCount_ = 0; // current blocky BLAS vertex count
|
mutable uint32_t rtBlockyVertexCount_ = 0; // current blocky BLAS vertex count
|
||||||
mutable uint32_t rtSmoothVertexCount_ = 0; // current smooth BLAS vertex count
|
mutable uint32_t rtSmoothVertexCount_ = 0; // current smooth BLAS vertex count
|
||||||
mutable uint32_t rtTopingVertexCount_ = 0; // current toping BLAS vertex count
|
mutable uint32_t rtTopingVertexCount_ = 0; // current toping BLAS vertex count
|
||||||
|
// BLAS capacity tracking: only recreate AS when vertex count exceeds capacity
|
||||||
|
mutable uint32_t blockyBLASCapacity_ = 0; // vertex count at BLAS creation
|
||||||
|
mutable uint32_t smoothBLASCapacity_ = 0;
|
||||||
|
mutable uint32_t topingBLASASCapacity_ = 0; // separate from topingBLASPositionCapacity_ (buffer capacity)
|
||||||
|
mutable uint32_t tlasInstanceCount_ = 0; // track TLAS instance count to avoid per-frame recreation
|
||||||
|
|
||||||
void dispatchBLASExtract(wi::graphics::CommandList cmd) const;
|
void dispatchBLASExtract(wi::graphics::CommandList cmd) const;
|
||||||
void buildAccelerationStructures(wi::graphics::CommandList cmd) const;
|
void buildAccelerationStructures(wi::graphics::CommandList cmd) const;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue