Skip to content

Track the dirty status of individual elements in AtomicSparseBufferVec.#24078

Open
pcwalton wants to merge 9 commits into
bevyengine:mainfrom
pcwalton:per-element-sparse-buffers
Open

Track the dirty status of individual elements in AtomicSparseBufferVec.#24078
pcwalton wants to merge 9 commits into
bevyengine:mainfrom
pcwalton:per-element-sparse-buffers

Conversation

@pcwalton

@pcwalton pcwalton commented May 2, 2026

Copy link
Copy Markdown
Contributor

Today, AtomicSparseBufferVec tracks the dirty status of individual pages of elements and performs a sparse upload when the number of modified pages is less than 15% of the total number of pages. (The default size of a page is 256 elements.) The reason why it doesn't track the dirty status of individual elements and instead only tracks pages is that it was assumed that frequently-changed elements would tend to cluster together, leading to low fragmentation. Unfortunately, though, this assumption has turned out to be false in practice. We extract meshes from the main world in parallel, so mesh instances end up scattered throughout in the MeshInputUniform buffer as the various extraction threads send the meshes they extract over a shared channel. Because of this, real-world workloads tend to dirty a disproportionately-large number of pages, even if they're only modifying a few mesh instances. The end result is that we rarely ever perform sparse updates unless no mesh instances have been updated at all, largely defeating the purpose of AtomicSparseBufferVec.

This patch fixes the issue by tracking the dirty status of individual elements, not just of individual pages. For efficiency, we now use a two-level atomically-updated bit vector to track the dirty status of elements. The lower level, dirty_bits, is a simple flat list of bits, one for each element and grouped into 64-bit words, 0 for "not modified" and 1 for "modified". The higher level, summary, contains one bit for each 64-bit word in the lower level, which is 0 if no elements in that word have been modified and 1 if at least one element in that word has been modified. (In other words, each bit in summary represents the logical or of every bit in the corresponding word in dirty_bits.) When searching for modified elements to upload sparsely, we use bit manipulation instructions on the summary words to skip up to 64 words in dirty_bits (i.e. 64² = 4096 elements) at a time.

Because the bit manipulation that this PR performs is tricky, it's factored out into separate functions that are individually tested via proptest randomized testing. This caught several bugs, some of which I believe to also be present in the existing code. Testing also verified that sparse buffer uploads are properly memory-bound as expected and that the dirty bit tracking has little overhead in practice.

The motivation for this PR was the discovery that bevy_city wasn't performing sparse uploads. Unfortunately, even with this patch, bevy_city still doesn't perform sparse uploads, because the number of moving cars (approximately 18%) exceeds 15% of the total mesh instances, and so sparse uploads aren't useful. I believe that bevy_city should be changed to increase the ratio of static buildings to cars in order to represent a more realistic workload. Once that's done, this patch should be helpful to help bevy_city scale, especially once transforms receive their own buffer.

`AtomicSparseBufferVec`.

Today, `AtomicSparseBufferVec` tracks the dirty status of individual
*pages* of elements and performs a sparse upload when the number of
modified pages is less than 15% of the total number of pages. (The
default size of a page is 256 elements.) The reason why it doesn't track
the dirty status of individual elements and instead only tracks pages is
that it was assumed that frequently-changed elements would tend to
cluster together, leading to low fragmentation. Unfortunately, though,
this assumption has turned out to be false in practice. We extract
meshes from the main world in parallel, so mesh instances end up
scattered throughout in the `MeshInputUniform` buffer as the various
extraction threads send the meshes they extract over a shared channel.
Because of this, real-world workloads tend to dirty a
disproportionately-large number of pages, even if they're only modifying
a few mesh instances. The end result is that we rarely ever perform
sparse updates unless no mesh instances have been updated at all,
largely defeating the purpose of `AtomicSparseBufferVec`.

This patch fixes the issue by tracking the dirty status of individual
elements, not just of individual pages. For efficiency, we now use a
two-level atomically-updated bit vector to track the dirty status of
elements. The lower level, `dirty_bits`, is a simple flat list of bits,
one for each element and grouped into 64-bit words, 0 for "not modified"
and 1 for "modified". The higher level, `summary`, contains one bit for
each 64-bit word in the lower level, which is 0 if no elements in that
word have been modified and 1 if at least one element in that word has
been modified. (In other words, each bit in `summary` represents the
logical *or* of every bit in the corresponding word in `dirty_bits`.)
When searching for modified elements to upload sparsely, we use bit
manipulation instructions on the summary words to skip up to 64 words in
`dirty_bits` (i.e. 64² = 4096 elements) at a time.

Because the bit manipulation that this PR performs is tricky, it's
factored out into separate functions that are individually tested via
`proptest` randomized testing. This caught several bugs, some of which I
believe to also be present in the existing code. Testing also verified
that sparse buffer uploads are properly memory-bound as expected and
that the dirty bit tracking has little overhead in practice.

The motivation for this PR was the discovery that `bevy_city` wasn't
performing sparse uploads. Unfortunately, even with this patch,
`bevy_city` still doesn't perform sparse uploads, because the number of
moving cars (approximately 18%) exceeds 15% of the total mesh instances,
and so sparse uploads aren't useful. I believe that `bevy_city` should
be changed to increase the ratio of static buildings to cars in order to
represent a more realistic workload. Once that's done, this patch should
be helpful to help `bevy_city` scale, especially once transforms receive
their own buffer.
@pcwalton pcwalton added the A-Rendering Drawing game state to the screen label May 2, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering May 2, 2026
@pcwalton pcwalton added C-Performance A change motivated by improving speed, memory usage or compile times C-Bug An unexpected or incorrect behavior S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 2, 2026
@cart cart closed this May 5, 2026
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in Rendering May 5, 2026
@cart cart reopened this May 5, 2026
@github-project-automation github-project-automation Bot moved this from Done to Needs SME Triage in Rendering May 5, 2026
Comment on lines +852 to +854
if old_final_dirty_bit_offset == BITS_PER_WORD - 1 {
old_final_summary_bit_offset += 1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

doesn't this overflow in the bitshift if this is 63?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think you’re right, but it actually ends up working because you overflow to 0, and then you subtract 1, so you set all the bits, which is what we intended to do. (That’s why the proptest based randomized testing never caught it.) But it’s certainly tricky code. I can add an extra test in there to make it clearer.

@Aceeri Aceeri Jun 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it might panic in debug mode if it does overflow too, might need overflow_shl/wrapping_shl or something (not braining which is the correct one here right now)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm actually confused now as to why the randomized testing never caught this!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, I looked into it, adding some assertions to verify things, and I believe it can’t happen because in that case you will be on the very edge of a summary block (i.e. the length will be divisible by 64 * 64 == 4096). In that case the if let Some(…) = … guard will fail. It works because the summary vector is always exactly as large as it needs to be, which itself is enforced by the fact that you can never pop individual elements from an AtomicSparseBufferVec, only push new elements and clear existing ones.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually this just resulted in a failure in CI! I guess the randomized testing just never found it before. I fixed it by adding a check to make sure it doesn’t overflow.

@kfc35 kfc35 self-requested a review June 26, 2026 18:16

@kfc35 kfc35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did not find any major logic issues. Just a few comments on documentation and the tests. Altogether not too difficult to comprehend, and the PR description and the documentation in the code help a lot!

Comment thread crates/bevy_render/src/render_resource/sparse_buffer_vec.rs Outdated
Comment thread crates/bevy_render/src/render_resource/sparse_buffer_update.wgsl Outdated
Comment thread crates/bevy_render/src/render_resource/sparse_buffer_vec.rs Outdated
Comment thread crates/bevy_render/src/render_resource/sparse_buffer_vec.rs
@pcwalton pcwalton added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

You added a new feature but didn't add a description for it. Please update the root Cargo.toml file.

@pcwalton

Copy link
Copy Markdown
Contributor Author

Failure looks legit, don’t merge.

@pcwalton pcwalton added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it labels Jun 27, 2026
@pcwalton pcwalton added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jun 27, 2026
@pcwalton

Copy link
Copy Markdown
Contributor Author

Ok, fixed. Should be good to go now.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jul 7, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 7, 2026
@pcwalton pcwalton added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it labels Jul 7, 2026
@pcwalton

pcwalton commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Test failure looks legit. Investigating.

@pcwalton

pcwalton commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

OK, the problem was just that the proptest was setting bits beyond the end of the sparse buffer dirty bits. Should be fixed now but let’s wait for a successful test run.

@pcwalton

pcwalton commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

OK, should be good to go.

@pcwalton pcwalton added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior C-Performance A change motivated by improving speed, memory usage or compile times S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

4 participants