fix(vk): dense grass cut off and far trees flickering without mesh shaders: fallback records from an allocator, 65535 slots #1511
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1511
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/mesh-fallback-dense-grass-record-allocator"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
On a GPU without mesh shaders, the fallback gives each meshlet slot records for its program's worst case, with only 1024 slots. The floor grass in Link's house starts about 13,900 meshlets, so almost none of it draws, and Hyrule Field's far trees need up to ~27,000. This takes records from a shared allocator instead, and raises the capacity to 65535 slots.
Builds on #1510 (it replaces that PR's slot count), so this PR also shows that commit; merge that one first. Part of splitting up #1023.
Problem
Each slot (one task thread's meshlet) gets records for
max primitives × corners. With 13,900 slots that would be about 650 MB, and at 1024 slots only the first 32 task workgroups' meshlets get any.Hyrule Field's far trees (the task/mesh pair
3cb76827/4f0c0b40, an indirect draw) start 721 to 849 task workgroups of 32 threads, measured withRX_VK_DRAW_STATS. At 16384 slots, the task workgroups past 512 started nothing. The culling list's order changes from frame to frame, so a different 29-40% of the trees was missing each frame, and the trees flickered continuously.Change
count × cornersrecords and adds them to the draw's vertex count. Records that don't fit aren't taken, so the draw always covers the first ones. The grass uses about 17 MB.MESH_FALLBACK_CAPACITYbecomes 65535, Vulkan's minimummaxComputeWorkGroupCount[0], which the mesh dispatch can reach. Only started slots run, so a draw that fits costs the same. The payload buffer is about 6.5 MB per recording thread at the largest payload seen (800 words).vkCmdDispatchIndirectfrom that count, so the CPU never needs the task count, whichDrawMeshTasksIndirectdoesn't record. A mesh workgroup reads its slot from the map in its entry block; its workgroup id is the slot modulo the task's threads, as natively.VertexIndex's record past the header, drawn withvkCmdDrawIndirectfrom the header.Known trade-off: primitives land in the order the workgroups took them, so blended mesh geometry may draw out of order. Lichii's fallback has the same trade-off.
Tests
tests/test_mesh_fallback.pychecks the allocator, the map dispatch, the header and the barrier.tests/fallback_buffer_cleanup_test.cpp's mock gains the three names the buffer code now uses. Its cases are unchanged, and all 9 pass.Tested on
17e8cd8with these PRs for the 65535 slots, 2026-11-10Read through the allocator, the map/indirect dispatch and the vertex-side indirect draw, and I couldn't find a problem.
Things I checked: thread 0 takes the records after a barrier that follows every thread's read of the primitive count, and the
base <= fit && taken <= fit - basetest is overflow-safe. The header reset sits behind a full barrier, andfull_barriercovers ALL_COMMANDS with memory read/write, so the indirect-argument and dispatch reads are covered. Task threads only write a map entry whenslot < MESH_FALLBACK_CAPACITY, so the dispatch count can't exceed 65535 and the payload sizing (map + ceil(cap/threads) payloads) holds. Themesh_fallback_slotload in the entry block dominates all its uses, andEmitWorkgroupIdfalls back to the raw id before the prologue has run. The disjoint header fills are right (no ordering between overlapping fills). CI is green.Nit: the 64 MB records area is allocated per recording thread, plus ~6.5 MB of payload, so VRAM use no longer scales with the program but is a fixed cost whenever a fallback draw happens. Fine for the 128 MB per-thread cap, just worth knowing on 4 GB cards.
Heads-up: this stacks on #1510 (still open), so that one needs to merge first, and #1747 also touches the mesh/task fallback, so the two will likely need reconciling.