Skip to content

Grow the sort indirect-dispatch buffer instead of overrunning a fixed 256 (closes #493)#538

Merged
djeedai merged 3 commits into
djeedai:mainfrom
stevesloan:fix-sort-indirect-buffer-capacity
Jun 20, 2026
Merged

Grow the sort indirect-dispatch buffer instead of overrunning a fixed 256 (closes #493)#538
djeedai merged 3 commits into
djeedai:mainfrom
stevesloan:fix-sort-indirect-buffer-capacity

Conversation

@stevesloan

Copy link
Copy Markdown
Contributor

Problem

With many ribbon effects alive (~260 trailed projectiles), the render thread crashes:

dispatch indirect: buffer uses bytes 3072..3084 which overruns indirect buffer of size 3072

(The crash reported in #493.)

Cause

The sort indirect-dispatch buffer is a growable GpuBuffer whose capacity is tracked in elements: allocate() may hand out an index past the current capacity, and prepare_buffers() then grows the GPU buffer to fit.

GpuBuffer::new_allocated took the element capacity as a separate argument, and SortBindGroups::new passed the buffer's byte size (3072) there instead of its element count. With 12-byte GpuDispatchIndirectArgs the physical buffer holds 256 elements, while GpuBuffer believed it had 3072 elements of headroom — so prepare_buffers() never grew it. The 257th sort dispatch then read past the end of the physical buffer (byte 3072 in a 3072-byte buffer), exactly the reported numbers.

Fix

Remove the separate capacity argument from new_allocated and derive the element capacity from the physical buffer (buffer.size() / item_size), so the recorded and physical capacities can't disagree. new_allocated has a single caller, which now just sizes the buffer; a debug_assert guards a non-multiple size.

Testing

Verified in a ribbon-heavy app (every projectile carries a ribbon trail): pre-fix it crashes reliably once ~256 ribbon sort dispatches are concurrently active; post-fix it runs well past that as the buffer grows. cargo build passes.

Independent of #537 (stale-buffer overruns in the sort bind-group cache); this is the buffer growth bug, #493.

Closes #493

stevesloan and others added 3 commits June 15, 2026 16:55
The sort indirect-dispatch buffer is a growable GpuBuffer whose capacity is
tracked in elements: allocate() may return an index past capacity, and
prepare_buffers() then grows the GPU buffer to fit. But GpuBuffer::new_allocated
took the element capacity as a separate argument, and SortBindGroups::new passed
the buffer's byte size (3072) there instead of its element count. With 12-byte
GpuDispatchIndirectArgs the physical buffer held 256 elements, while GpuBuffer
believed it had 3072 elements of headroom, so prepare_buffers() never grew it.
Once a ribbon-heavy scene needed a 257th sort dispatch, the indirect dispatch
read past the end:

    dispatch indirect: buffer uses bytes 3072..3084 which overruns indirect
    buffer of size 3072

(~260 trailed projectiles, matching the issue report.)

Remove the separate capacity argument from new_allocated and derive the element
capacity from the physical buffer (buffer.size() / item_size), so the recorded
and physical capacities cannot disagree. new_allocated has a single caller,
which now just sizes the buffer; a debug_assert guards a non-multiple size.

Closes djeedai#493

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@djeedai djeedai added C - bug Something isn't working A - internal Internal change on a core system labels Jun 20, 2026
@djeedai djeedai merged commit 32b042c into djeedai:main Jun 20, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A - internal Internal change on a core system C - bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Indirect buffer overrun

2 participants