Skip to content

Commit 972322c

Browse files
committed
docs(missing): record the CNA Vulkan renderer's per-draw UBO overrun
Building MeshCraft with -DMESH_CRAFT_GRAPHICS_BACKEND=VULKAN works and the editor runs, but lighting on some objects flickers frame to frame in scenes with many lit objects -- reproduced on test/medieval_castle.mc3.xml against CNA develop @ 1bb2145d9, on a real Intel Iris Xe. The Vulkan validation layer names it every frame: pDynamicOffsets[0] runs 131072..133376 in steps of 256, i.e. ring-buffer slots 512..521 of a 512-slot buffer (VUID-vkCmdBindDescriptorSets-pDescriptorSets-01979). The cause is at VulkanRenderer.cpp:9210 -- the bounds guard covers the host-side memcpy but not the vkCmdBindDescriptorSets that follows it, so a draw past slot 512 binds an out-of-range offset and reads whatever memory follows the buffer instead of its own lighting constants. That memory changes between frames, hence the flicker; that the guard covers only the memcpy is why this shows up as wrong pixels rather than host-memory corruption. All six per-draw UBO paths share the pattern (DualTexFog, EnvMap, Skinned, SkinnedFog, LitTextured, FogTex3D), so it is one pattern to fix rather than one site. Skinned/SkinnedFog cap at 32 draws and so break far earlier. This is a CNA-side defect and this repo's CLAUDE.md forbids CNA changes without the owner's permission, so it is written down here instead of fixed. The section carries the reproduction, the root cause, the affected table, two candidate fixes, and -- so the scope is not overstated -- what does work on VULKAN today: it builds clean, runs, and renders a pixel-correct headless screenshot with zero validation errors, because a single frame never reaches slot 512. That is precisely why the defect hides in one-shot renders. missing.md is otherwise about mc3 format coverage; the intro and the summary table now both flag this section as a deliberate scope exception.
1 parent f1588ab commit 972322c

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

missing.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ covered by `plan.md`'s `AUD-###`/`SYS-W7-##` rows. This is specifically about
1111
editor UI gaps: format features that parse/round-trip correctly but have no
1212
interactive editing surface, or have one that's incomplete.
1313

14+
One exception to that scope is recorded deliberately: the
15+
"Backend gap" section immediately below documents a CNA Vulkan renderer defect
16+
found while qualifying the VULKAN backend. It is kept here so it is not lost,
17+
and is marked as out-of-scope where it appears.
18+
1419
Findings are grouped by severity: **still-open total gaps** (zero UI,
1520
XML-hand-edit only) first, then **still-open partial gaps** (UI exists but
1621
incomplete or buggy), then **non-gaps** worth noting for context, then two
@@ -21,6 +26,122 @@ was open as of 2026-07-18 closed the same day.
2126

2227
---
2328

29+
## Backend gap: the CNA Vulkan renderer overruns its per-draw uniform buffers
30+
31+
*Scope note: everything else in this file is about mc3 format coverage in the
32+
editor UI. This section is a deliberate exception — it records a rendering
33+
defect found while qualifying the VULKAN backend, so it is not lost. It is a
34+
**CNA-side** bug (`cna/modules/renderers/vulkan`), not a MeshCraft one, and
35+
per this repo's `CLAUDE.md` ("No CNA changes without owner permission") it is
36+
reported here rather than fixed.*
37+
38+
Found 2026-08-20 against CNA `develop` @ `1bb2145d9`, on real hardware
39+
(Intel Iris Xe Graphics, ADL GT2; Mesa, Vulkan 1.4.305).
40+
41+
### Symptom
42+
43+
Building MeshCraft with `-DMESH_CRAFT_GRAPHICS_BACKEND=VULKAN` works and the
44+
editor runs, but **lighting on some objects flickers frame to frame** in
45+
scenes with many lit objects. It is not the whole scene — only a tail of the
46+
objects drawn in each frame is affected. Observed interactively on
47+
`test/medieval_castle.mc3.xml` (272 instances, 202 boxes, 89 cylinders,
48+
31 spheres).
49+
50+
The application does not crash; a `timeout 20` run had to be killed, it never
51+
exited on its own.
52+
53+
### Evidence
54+
55+
The Vulkan validation layer reports, every frame:
56+
57+
```
58+
vkCmdBindDescriptorSets(): pDynamicOffsets[0] is 131072, which when added to
59+
the buffer descriptor's range (256) and offset (0) is greater than the size
60+
of the buffer (131072) in descriptorSet #0 binding #1 descriptor[0].
61+
VUID-vkCmdBindDescriptorSets-pDescriptorSets-01979
62+
```
63+
64+
Reported offsets run 131072 → 133376 in steps of 256, i.e. ring-buffer slots
65+
512–521: ten draws per frame past the end of a 512-slot buffer.
66+
67+
### Root cause
68+
69+
`cna/modules/renderers/vulkan/src/VulkanRenderer.cpp:9210` (the lit-textured
70+
path, the one that carries lighting constants):
71+
72+
```cpp
73+
const uint32_t slot = litTexturedUBOSlot++;
74+
const uint32_t uboOff = slot * kLitTexturedUBOStride;
75+
if (uboOff + 256 <= kLitTexturedUBOStride * kLitTexturedUBOMaxDraws) {
76+
std::memcpy(static_cast<uint8_t*>(litTexturedUBOPtr_[currentFrame_]) + uboOff,
77+
draw.litUboData, 256); // <-- bounds-checked
78+
}
79+
vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS,
80+
pipelineLayoutLitTextured3D_, 0, 1,
81+
&draw.litTexturedDescSet, 1, &uboOff); // <-- NOT checked
82+
```
83+
84+
The ring buffer is `kLitTexturedUBOStride (256) * kLitTexturedUBOMaxDraws (512)`
85+
= 131072 bytes. Once a frame issues more than 512 lit-textured draws, the guard
86+
correctly skips the host-side write — but the descriptor bind still happens at
87+
the out-of-range offset. Those draws therefore read whatever memory follows the
88+
buffer instead of their own lighting constants, and since that memory changes
89+
between frames, the affected objects flicker. That the guard covers only the
90+
`memcpy` is exactly why the failure is a *visual* one rather than a host-memory
91+
corruption.
92+
93+
### Scope: all six per-draw UBO paths share the pattern
94+
95+
The bind sits outside the bounds guard in every one of them, so this is a
96+
pattern to fix once, not a single site:
97+
98+
| Path | `VulkanRenderer.cpp` | Slots (`stride × maxDraws`) |
99+
|------|----------------------|-----------------------------|
100+
| DualTexFog | 9101 | 256 × 512 |
101+
| EnvMap | 9116 | 256 × 512 |
102+
| Skinned | 9132 | 4608 × **32** |
103+
| SkinnedFog | 9146 | 256 × **32** |
104+
| LitTextured | 9212 | 256 × 512 |
105+
| FogTex3D | 9231 | 256 × 512 |
106+
107+
`Skinned`/`SkinnedFog` cap at 32 draws, so they break far earlier than the
108+
512-slot paths — a scene with more than 32 skinned draws in a frame is already
109+
past the end.
110+
111+
### Possible fixes (for whoever picks this up)
112+
113+
1. **Grow the ring buffer on demand**, appending chunks as the cursor runs past
114+
the current allocation. This is the approach the compiled-effect path in the
115+
same file already takes (`EnsureCompiledEffectUniformChunkEXT`,
116+
`VulkanRenderer.cpp:6865`), so it is a pattern the renderer already carries
117+
rather than a new mechanism. Preferred: it is the only option that keeps
118+
rendering correct at any scene size.
119+
2. **Move the bind inside the guard**, skipping the draw entirely when the slot
120+
is out of range. Correctness-safe but visibly lossy — affected objects
121+
disappear instead of flickering — and silently so.
122+
123+
Whichever is chosen, an overflow should not be silent: at minimum log once per
124+
frame that N draws exceeded the capacity.
125+
126+
### What does work on VULKAN
127+
128+
For the record, so the scope of this defect is not overstated:
129+
130+
- MeshCraft configures, builds and links clean against
131+
`-DMESH_CRAFT_GRAPHICS_BACKEND=VULKAN` (zero compile errors).
132+
- `MeshCraft --version` runs; `isBackendSupportedAlg()` already admits VULKAN.
133+
- A headless `--screenshot` render of `test/all_primitives.mc3.xml` is
134+
**pixel-correct and produces zero validation errors** — a single frame never
135+
reaches slot 512, which is precisely why the defect hides in one-shot renders
136+
and only shows up in sustained interactive use.
137+
- Source-GLSL ShaderEffects (Bloom, SSAO, skybox shading, material preview)
138+
are unavailable on this backend and disable themselves with a startup
139+
message. That is a separate, already-documented limitation
140+
(`supportsTextShaderEffectsAlg()` in `include/MeshCraft/GraphicsBackendCheck.hpp`),
141+
not part of this defect.
142+
143+
---
144+
24145
## Resolved since 2026-07-18
25146
26147
All 4 "total gap" findings (N8/N9/N10/N11), plus the texture file-browse
@@ -167,6 +288,7 @@ declared — see the Summary table below and `SYS-W14-14`.
167288
168289
| Area | Status |
169290
|------|--------|
291+
| CNA Vulkan renderer: per-draw UBO overrun (lighting flicker) | 🔴 open, CNA-side — see "Backend gap" section above |
170292
| N1-N7 (SVG textures, embeds, scripts, audio, triggers, scene states, meta) | ✅ resolved (`STAB-0703`..`0709`) |
171293
| N8 Object `scriptId` attachment | ✅ resolved (`SYS-W14-10`) |
172294
| N9 Library metadata / imports (`.mc3lib`) | ✅ resolved (`SYS-W14-13`) |

0 commit comments

Comments
 (0)