Skip to content

fix(vk): size the mesh index map in whole 32-primitive blocks - #2312

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
Kolkka:fix/mesh-index-map-blocks
Nov 11, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
Kolkka:fix/mesh-index-map-blocks

Conversation

@Kolkka

@Kolkka Kolkka commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Flashing triangles over the foliage and ground in Kokiri Forest: a stray vertex from mesh programs with 126 primitives, which are most of OoT's.

What changed: a mesh program writes the index bytes of every 32-primitive block, including lanes past max_primitives (a count of 0 wraps min(count - 1, lane) onto every lane). The index map was sized to max_primitives, so those writes were clamped onto its last byte, the last primitive's last corner. DefineMesh now sizes the map like the attributes, in whole blocks of 32 primitives. nxnative found the same thing ("primitive index bytes for every primitive block"). tests/test_mesh_point_topology.py checks the new size.

Tested: Steam Deck LCD, Mesa RADV, Kokiri Forest. I compared 150–200 screenshots per run, each against the previous one: no flashes remain, and the biggest changes are Navi moving and the lighting fading in. tests/run_all.py: no new failures against main.

image

🤖 Generated with Claude Code

Flashing triangles over Kokiri's foliage and ground: mesh programs write the
index bytes of every 32-primitive block, lanes past max_primitives too, and
with 126 primitives (most of OoT's) the writes past the map were clamped onto
the last primitive's last corner, a stray vertex. The map is now sized like the
attributes, in whole blocks of 32 primitives. test_mesh_point_topology checks
the new size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tiHucJGVjM9vhcGkzmPMu

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks right to me. The map is laid out linearly (byte 4 + corners * primitive + corner, both in the epilogue and in the fallback's record copy), so a program that writes index bytes for a whole 32-lane primitive block reaches up to 4 + corners * DivCeil(max_primitives, 32) * 32 - 1. With the old size, EmitIsbeWriteMapU8's Clamped(address, mesh_map_bytes) folded every out-of-range byte onto the last one, so for 126 primitives the 6 bytes of lanes 126/127 raced with primitive 125's last corner. That fits the flashing-triangle symptom well, and sizing the map in whole blocks matches how mesh_attribute_words already sizes the primitive attributes.

Other things I checked:

  • Readers only look at primitives < count (bounded by max_primitives), so the extra bytes are scratch and never read back. The prologue zeroing walks the new size too.
  • mesh_map is defined before the if (mesh_fallback) return;, so the compute fallback path gets the fix as well, not only native mesh shaders.
  • Cost: at most 31 * 3 extra workgroup words (~372 bytes) per mesh workgroup, which is nothing next to the attribute region.
  • max_primitives == 0 still gives 4 bytes, same as before.
  • The test change tracks the new expression, nothing weakened.

No overlap with #2311 (vk_draw.cpp) or #2362 (vk_main.cpp / vk_menu.cpp); they touch different files and this fix helps whichever mesh path ends up the default on RADV.

CI: only the title check has run; the build and game-free regression workflows are waiting for approval to run on this fork, so those should go green before merging.

@hckrman101
hckrman101 merged commit f9286c4 into NotRexed:main Nov 11, 2026
2 checks passed
NicolasHenrique5648 pushed a commit to NicolasHenrique5648/Hookshot that referenced this pull request Nov 11, 2026
…ed#2312)

Flashing triangles over the foliage and ground in Kokiri Forest: a stray
vertex from mesh programs with 126 primitives, which are most of OoT's.

**What changed:** a mesh program writes the index bytes of every
32-primitive block, including lanes past `max_primitives` (a count of 0
wraps `min(count - 1, lane)` onto every lane). The index map was sized
to `max_primitives`, so those writes were clamped onto its last byte,
the last primitive's last corner. `DefineMesh` now sizes the map like
the attributes, in whole blocks of 32 primitives. nxnative found the
same thing ("primitive index bytes for every primitive block").
`tests/test_mesh_point_topology.py` checks the new size.

**Tested:** Steam Deck LCD, Mesa RADV, Kokiri Forest. I compared 150–200
screenshots per run, each against the previous one: no flashes remain,
and the biggest changes are Navi moving and the lighting fading in.
`tests/run_all.py`: no new failures against `main`.

<img width="1269" height="712" alt="image"
src="https://github.com/user-attachments/assets/9b159590-ee90-4b68-bf39-ef0373c56ad4"
/>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants