Skip to content

Conversation

ElliottjPierce
Copy link
Contributor

Objective

The goal here is to provide a general alternative to a RwLock for rarely changed collections. When a change is made, it stages it in temporary storage without actually modifying the data. Then, at user defined points, the changes are applied to the data, draining the staged changes.

This has come up a lot regarding components, ex: #17569, but the same treatment has been discussed for reflection and other areas where a type has to be registered once and only once, on demand. This allows that functionality to be put on separate threads, which could unblock a lot of additional ideas, like assets as entities and read-only queries.

Further, it may be useful to put these "StageOnWrite" data structures in an Arc, so that has been implemented too. This could unlock benefits like not having to translate component ids when spawning scenes or cloning entities, but more on that in future PRs and discussions.

There's a lot of things this PR benefits but it should be evaluated as a stand-alone addition for these utilities. Still, those goals and use cases did influence the design, so keep that in mind.

Solution

  • Implement a generic StagedChanges trait that tracks changes to a "target" data structure, Cold.
  • Implement types that track these staged changes, StageOnWrite and AtomicStageOnWrite.
  • Implement both locking and unlocked ways of accessing the staged and un-staged data together, mutably and immutably.

Testing

I did create a test module with an example, but the potential for bugs here is mostly in systems using this feature, so I kept it kind of minimal for now.

@bushrat011899
Copy link
Contributor

The no_std CI failure can be resolved by feature-gating your new types behind feature = "alloc"

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Can we put this in bevy_platform_support? I think it's likely more fitting there, and I'm nervous about adding anything new to bevy_utils, since we're trying to eliminate it.

@ElliottjPierce
Copy link
Contributor Author

Can we put this in bevy_platform_support? I think it's likely more fitting there, and I'm nervous about adding anything new to bevy_utils, since we're trying to eliminate it.

Ah. I wasn't aware of that. I can absolutely move it.

@bushrat011899
Copy link
Contributor

I'd personally prefer if it stayed in bevy_utils until it could get yeeted into it's own micro crate. The goal for bevy_platform_support is to just get std-like functionality for all platforms and I'd like to keep its scope narrowed to that, otherwise I see it just being more bevy_utils. But I'm willing to be overruled on that front if we consider this something that should be in the standard library.

Hopefully this also fixes no_std
Copy link
Contributor

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

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

This should fix the alloc issue you were having before and also allow using most of the functionality here even without alloc enabled.

@NthTensor
Copy link
Contributor

Great, thanks for the thorough response. Seems like we should def proceed with this then.

@ElliottjPierce
Copy link
Contributor Author

@bushrat011899 I'm satisfied with where it's at at the moment. You had a lot of great suggestions in the last round. Sorry it took so long to do them. Looking forward to more!

Copy link
Contributor

@andriyDev andriyDev left a comment

Choose a reason for hiding this comment

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

I haven't yet reviewed the implementation, but I need a break, so here's most of the review. I'll come back for another pass on the impl later.

/// This trait provides some conviniencies around [`StagableWritesCore`].
///
/// For example, mutable references are used to enforce safety for some functions.
pub trait StagableWrites {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this need to be a separate trait? Can't we just impl StagableWritesCore { /*methods go here*/ }?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thats a really good question.

StagableWritesCore gives information about the type coordinating the stage-on-write solution. This is the core of the actual implementation. StagableWrites protects against deadlocks by requiring mutable access to a reference to the StagableWritesCore type, not to the inner type itself. That reference could be a ArcStageOnWrite or a RefStageOnWrite.

As an example, let's say we have a ArcStageOnWrite in thread A, and we share that reference to threads B and C by cloning the arc. Thread's B and C can prevent internal deadlocks by using StagableWrites on their arcs (provided they don't clone the arcs internally). If the two traits were unified, we would need to clone a mutable reference between threads (not allowed) or use the unsafe, deadlock-prone writing methods on StagableWritesCore.

The extra trait StagableWrites was the best way I could think of to balance deadlock prevention with thread sharing. If you have any other ideas, I'm all ears.

@ElliottjPierce
Copy link
Contributor Author

@andriyDev Thanks for the great docs review. I don't know how I managed some of those typos lol.

Co-Authored-By: andriyDev <andriydzikh@gmail.com>
@alice-i-cecile
Copy link
Member

Closing in favor of #18173.

github-merge-queue bot pushed a commit that referenced this pull request Mar 10, 2025
# Objective

This is an alternative to #17871 and #17701 for tracking issue #18155.
This thanks to @maniwani for help with this design.

The goal is to enable component ids to be reserved from multiple threads
concurrently and with only `&World`. This contributes to assets as
entities, read-only query and system parameter initialization, etc.

## What's wrong with #17871 ?

In #17871, I used my proposed staging utilities to allow *fully*
registering components from any thread concurrently with only
`&Components`. However, if we want to pursue components as entities
(which is desirable for a great many reasons. See
[here](https://discord.com/channels/691052431525675048/692572690833473578/1346499196655505534)
on discord), this staging isn't going to work. After all, if registering
a component requires spawning an entity, and spawning an entity requires
`&mut World`, it is impossible to register a component fully with only
`&World`.

## Solution

But what if we don't have to register it all the way? What if it's
enough to just know the `ComponentId` it will have once it is registered
and to queue it to be registered at a later time? Spoiler alert: That is
all we need for these features.

Here's the basic design:

Queue a registration:

1. Check if it has already been registered.
2. Check if it has already been queued.
3. Reserve a `ComponentId`.
4. Queue the registration at that id.

Direct (normal) registration:

1. Check if this registration has been queued.
2. If it has, use the queued registration instead.
3. Otherwise, proceed like normal.

Appllying the queue:

1. Pop queued items off one by one.
2. Register them directly.

One other change:

The whole point of this design over #17871 is to facilitate coupling
component registration with the World. To ensure that this would fully
work with that, I went ahead and moved the `ComponentId` generator onto
the world itself. That stemmed a couple of minor organizational changes
(see migration guide). As we do components as entities, we will replace
this generator with `Entities`, which lives on `World` too. Doing this
move early let me verify the design and will reduce migration headaches
in the future. If components as entities is as close as I think it is, I
don't think splitting this up into different PRs is worth it. If it is
not as close as it is, it might make sense to still do #17871 in the
meantime (see the risks section). I'll leave it up to y'all what we end
up doing though.

## Risks and Testing

The biggest downside of this compared to #17871 is that now we have to
deal with correct but invalid `ComponentId`s. They are invalid because
the component still isn't registered, but they are correct because, once
registered, the component will have exactly that id.

However, the only time this becomes a problem is if some code violates
safety rules by queuing a registration and using the returned id as if
it was valid. As this is a new feature though, nothing in Bevy does
this, so no new tests were added for it. When we do use it, I left
detailed docs to help mitigate issues here, and we can test those
usages. Ex: we will want some tests on using queries initialized from
queued registrations.

## Migration Guide

Component registration can now be queued with only `&World`. To
facilitate this, a few APIs needed to be moved around.

The following functions have moved from `Components` to
`ComponentsRegistrator`:

- `register_component`
- `register_component_with_descriptor`
- `register_resource_with_descriptor`
- `register_non_send`
- `register_resource`
- `register_required_components_manual`

Accordingly, functions in `Bundle` and `Component` now take
`ComponentsRegistrator` instead of `Components`.
You can obtain `ComponentsRegistrator` from the new
`World::components_registrator`.
You can obtain `ComponentsQueuedRegistrator` from the new
`World::components_queue`, and use it to stage component registration if
desired.

# Open Question

Can we verify that it is enough to queue registration with `&World`? I
don't think it would be too difficult to package this up into a
`Arc<MyComponentsManager>` type thing if we need to, but keeping this on
`&World` certainly simplifies things. If we do need the `Arc`, we'll
need to look into partitioning `Entities` for components as entities, so
we can keep most of the allocation fast on `World` and only keep a
smaller partition in the `Arc`. I'd love an SME on assets as entities to
shed some light on this.

---------

Co-authored-by: andriyDev <andriydzikh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Utils Utility functions and types C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants