- Load CC0 FreeStylized textures (6 materials: grass, dirt, stone, sand, snow, smoothstone) as Texture2DArray: t1=albedo+heightmap RGBA, t7=normal maps GL format - Height-based texture blending: winner-takes-all with sharpness=16, 40% blend zone, asymmetric bias (coeff 1.6) for resistBleed materials (grass resists sand bleed) - UDN triplanar normal mapping with 3 critical fixes: * Use raw normal (NOT abs) in UDN formula — abs inverts lighting on -X/-Y/-Z faces * sign(normal) correction on tangent X for back-facing UV mirror * GL green channel flip on Y-projection only (not X/Z where V=worldY is correct) - Dirt material rendered smooth (FLAG_SMOOTH), ground_02 texture darkened 0.75 - Sun orbit debug mode (F7): 10s cycle with sinusoidal altitude - Crosshair + face debug HUD (F8): DDA raycast, camera/target/face/normal info - Screenshot F6 now writes companion .log file with full debug state - Document UDN pitfalls and logical vs physical coordinates in TROUBLESHOOTING.md - Add tools/prepare_textures.py for texture pipeline (ZIP → albedo+height RGBA + normal)
18 KiB
BVLE Voxels — Troubleshooting & Pièges techniques
Table des matières
- APIs Wicked utilisées
- Coordonnées logiques vs physiques
- Triplanar UDN Normal Mapping
- Shaders custom — Pièges importants
- CreateBuffer avec capacity > data size
- BLAS/TLAS per-frame recreation — VRAM leak
- Diagnostics et debugging
- Gestion des resource states DX12 (buffers)
APIs Wicked utilisées
| Besoin | API Wicked |
|---|---|
| Clavier WASD | wi::input::Down(CHARACTER_RANGE_START + offset) (pas de KEYBOARD_BUTTON_W) |
| Souris delta | wi::input::GetMouseState().delta_position |
| Cacher curseur | wi::input::HidePointer(bool) |
| Shader loading | wi::renderer::LoadShader() - compile auto les .hlsl en .cso si absent |
| PSO states | wi::renderer::GetRasterizerState() etc. retournent des pointeurs (pas besoin de &) |
| Render pass | RenderPassImage::RenderTarget(texture, loadOp, storeOp, layoutBefore, layoutAfter, subresource=-1) |
| Font overlay | wi::font::Params est un struct - setter les membres un par un |
| Camera | CameraComponent::At est une direction (utilisé avec XMMatrixLookToLH), pas un point cible |
| Buffer create | device->CreateBuffer(desc, raw_data_ptr, buffer) — PAS de SubresourceData pour les buffers ! |
| Texture create | device->CreateTexture(desc, subresourceData_ptr, texture) — utilise SubresourceData* (différent de CreateBuffer) |
| Buffer update | device->UpdateBuffer(buffer, data, cmd, size, offset) |
| Push constants | device->PushConstants(data, size, cmd) — mappés à register(b999), taille fixe 48 bytes (12 × uint32) |
| Command list | device->BeginCommandList() — nouveau cmd list pour render passes séparés |
| Render pass | NE JAMAIS imbriquer ! Un seul render pass actif par command list |
| Debug DX12 | Passer "debugdevice" en argument pour activer la couche de debug D3D12 |
| Logging | wi::backlog::post(message, logLevel) — préférer au logging fichier |
| Screen size (draw) | GetLogicalWidth()/GetLogicalHeight() pour wi::font et wi::image (PAS GetPhysicalWidth) |
| Solid rect draw | wi::image::Draw(wi::texturehelper::getWhite(), params, cmd) — ne PAS passer nullptr |
Coordonnées logiques vs physiques — Piège majeur
Wicked Engine distingue deux systèmes de coordonnées écran :
- Physical (
GetPhysicalWidth()/GetPhysicalHeight()) : pixels réels du backbuffer. Utilisé pour créer les render targets, viewports, et textures GPU. - Logical (
GetLogicalWidth()/GetLogicalHeight()) : pixels DPI-scaled. Tout le système 2D de Wicked (wi::font::Draw,wi::image::Draw,wi::image::Params::pos/siz) travaille en coordonnées logiques.
Symptôme : éléments HUD décalés, crosshair excentré, texte hors écran.
// ❌ FAUX — décalé si DPI scaling ≠ 100%
float cx = (float)GetPhysicalWidth() * 0.5f;
wi::font::Params fp; fp.posX = cx;
// ✅ CORRECT
float cx = GetLogicalWidth() * 0.5f;
wi::font::Params fp; fp.posX = cx;
Pour dessiner un rectangle solide (pas de texture) :
// ❌ FAUX — ne dessine rien
wi::image::Draw(nullptr, params, cmd);
// ✅ CORRECT — utiliser la texture blanche 1x1 intégrée
#include "wiTextureHelper.h"
wi::image::Draw(wi::texturehelper::getWhite(), params, cmd);
La projection 2D est définie dans wiCanvas.h :
GetProjection() = XMMatrixOrthographicOffCenterLH(0, GetLogicalWidth(), GetLogicalHeight(), 0, -1, 1);
Triplanar UDN Normal Mapping — Pièges majeurs
L'implémentation UDN (Unreal Derivative Normal) triplanar pour les normal maps a trois subtilités critiques :
1. NE PAS utiliser abs(normal) dans la formule UDN
La référence Ben Golus utilise abs(normal) car elle cible des terrains (normales toujours vers le haut). Pour des voxels avec 6 directions de faces, abs() force la composante dominante à être positive, inversant l'éclairage sur les faces -X, -Y et -Z.
// ❌ FAUX — inverse les normales sur 3 faces (le NdotL est faux)
float3 absN = abs(normal);
float3 worldNX = float3(tnX.xy + absN.zy, absN.x).zyx;
// Face -X: absN.x = 1 → résultat pointe vers +X au lieu de -X
// ✅ CORRECT — utiliser le normal brut
float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx;
// Face -X: normal.x = -1 → résultat pointe bien vers -X
Diagnostic : ombres RT correctes (elles utilisent la géométrie) mais éclairage direct inversé sur certaines faces → contradiction visuelle.
2. Correction de signe pour les faces négatives
Les UV sont miroir sur les faces négatives. Le sign(normal) corrige la composante tangent-space X :
float3 axisSign = sign(normal);
tnX.x *= axisSign.x; // Flip U-tangent pour -X
tnY.x *= axisSign.y; // Flip U-tangent pour -Y
tnZ.x *= axisSign.z; // Flip U-tangent pour -Z
3. Flip green channel pour les normal maps OpenGL (seulement projection Y)
Les textures normal_gl ont le green channel inversé par rapport à DX. En triplanar, seule la projection Y (faces horizontales, UV=xz) nécessite le flip — les projections X et Z ont V=world Y qui est naturellement correct.
// ❌ FAUX — casse les faces verticales
tnX.y = -tnX.y; tnY.y = -tnY.y; tnZ.y = -tnZ.y;
// ✅ CORRECT — seulement la projection Y
tnY.y = -tnY.y;
Formule complète correcte :
float3 axisSign = sign(normal);
float3 tnX = sample(wp.zy).rgb * 2.0 - 1.0;
float3 tnY = sample(wp.xz).rgb * 2.0 - 1.0;
float3 tnZ = sample(wp.xy).rgb * 2.0 - 1.0;
tnY.y = -tnY.y; // GL flip Y-projection only
tnX.x *= axisSign.x; // sign correction
tnY.x *= axisSign.y;
tnZ.x *= axisSign.z;
float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx; // RAW normal
float3 worldNY = float3(tnY.xy + normal.xz, normal.y).xzy;
float3 worldNZ = float3(tnZ.xy + normal.xy, normal.z);
return normalize(worldNX * w.x + worldNY * w.y + worldNZ * w.z);
Shaders custom — Pièges importants
Les shaders custom doivent respecter le binding model de Wicked Engine :
1. Root signature obligatoire
Chaque shader DOIT avoir une root signature DX12 intégrée, soit via #include "globals.hlsli" (auto), soit via [RootSignature(MACRO)] sur le entry point.
2. Root signature Wicked (HLSL 6.6+)
b999→ push constants (12 × uint32 = 48 bytes max)b0, b1, b2→ CBV root descriptorst0-t15, u0-u15→ dans une descriptor table partagées0-s7→ samplers dynamiquess100-s109→ static samplers (linear, point, aniso, etc.)
3. Chemins des shaders
SHADERPATH=<exe_dir>/shaders/hlsl6/— où les.csocompilés sont stockésSHADERSOURCEPATH=../../engine/WickedEngine/shaders/— où les.hlslsources sont cherchés- Les shaders custom doivent être copiés dans
SHADERSOURCEPATH(sous-dossiervoxel/) LoadShader(stage, shader, "voxel/voxelVS.cso")→ compileSHADERSOURCEPATH/voxel/voxelVS.hlslsi.csoabsent
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-facingcross(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 :
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()quandstate_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 :
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 :
- BLAS : Créer avec
vertex_count = capacity(25% headroom). Avant chaqueBuildRaytracingAccelerationStructure, mettre à jourdesc.bottom_level.geometries[0].triangles.vertex_countavec 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é.
// 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);
-
TLAS : Ne recréer que quand le nombre d'instances change (ex: 2→3 quand la toping BLAS devient valide). Sinon,
BuildRaytracingAccelerationStructuresuffit car les pointeurs BLAS restent stables (les BLAS ne sont pas recréés). -
Deferred upload pattern : Les positions BLAS toping sont uploadées via
UpdateBufferdansRender()(flagtopingBLASDirty_) avant le BLAS rebuild, carUpdate()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 + adressesbvle_crash.dmp: minidump analysable avec Visual Studio- Nécessite
dbghelp.libet build avec symbols (RelWithDebInfoouDebug)
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 invalideDXGI_ERROR_DEVICE_HUNG→ shader en boucle infinie ou accès mémoire hors limites- Dialog bloquant avec
messageBox→ vient dewi::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 :
- NE PAS utiliser
timeoutpour tester — demander à l'utilisateur de lancer manuellement - Vérifier
bvle_backlog.txtaprès exécution (contient les erreurs DX12) - Vérifier
bvle_crash.logetbvle_crash.dmppour les crashs SEH - Lancer avec
debugdevicepour obtenir les messages de validation D3D12 détaillés dans le backlog - Un exit code non-zéro n'est PAS fiable :
timeoutrenvoie 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()→ appelleCopyBufferRegionsans 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. AppelerUpdateBufferdessus au frame suivant → crash car pas de transition INDIRECT_ARGUMENT → COPY_DST. - Les buffers créés avec
Usage::DEFAULTdé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 ResourceStateet faire des barriers explicites entre chaque usage.