Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Deterministic mutation hooks for ECS#87

Closed
caniko wants to merge 2 commits into
bevyengine:mainfrom
caniko:deterministic-mutation-rfc
Closed

Deterministic mutation hooks for ECS#87
caniko wants to merge 2 commits into
bevyengine:mainfrom
caniko:deterministic-mutation-rfc

Conversation

@caniko
Copy link
Copy Markdown

@caniko caniko commented May 31, 2026

RFC: Deterministic ECS mutation primitives

This RFC proposes two bevy_ecs primitives for deterministic mutation pipelines:

  • ObserverSet and ordered observer dispatch, matching the familiar .before / .after / .in_set / .chain() scheduling vocabulary.
  • restricted component access through #[component(restricted)] and RestrictedMut<T>, an opt-in safe write gate that prevents ordinary safe in-place mutable ECS access from bypassing a mediated mutation path.

Rendered RFC:

Related Bevy work:

Important provenance note for review:

  • bevy#24370 is currently closed, and its discussion includes Bevy AI-policy concerns. This RFC is intended to discuss the ECS design. It is not intended to bypass provenance requirements for any implementation PR. Any reopened or successor implementation PR must independently satisfy Bevy's contribution policy.

The RFC deliberately does not propose upstreaming MutationLog, SyncBarrier, networking, save/load, undo, replay, or audit policy. Those remain library concerns built on top of the two proposed engine-level enforcement hooks.

Specific design points where review would be especially useful:

  • whether the restricted spelling should be #[derive(Component)] #[component(restricted)] as recommended here, or the prototype's #[derive(RestrictedAccess)];
  • whether RestrictedMut<T>::get_mut should be included in v1 or whether the API should start with closure-style modify plus iteration;
  • whether the RFC's listed safe mutable access surface is complete enough for v1, especially the safe untyped ComponentId paths returning MutUntyped.

caniko added 2 commits May 31, 2026 12:34
Two bevy_ecs primitives for deterministic mutation pipelines:
ObserverSet (ordered observer dispatch) and restricted component
access (#[component(restricted)] + RestrictedMut<T>).
@amtep
Copy link
Copy Markdown

amtep commented Jun 1, 2026

Can you spell out what the use case is for restricted mutability when compared to making the component's fields private and providing mutation metbods?

@SpecificProtagonist
Copy link
Copy Markdown

SpecificProtagonist commented Jun 1, 2026

It is not clear to me how RestrictedMut gives the component author any control over what mutations happen/the ability to observe it? Any downstream crate can call modify() just the same.

Also, I assume that the reason for not using immutable components is that inserting components requires you to have ownership of a full component value, even if the data is expensive or impossible to clone? But the RFC needs to state why they can't be used.

Comment on lines +59 to +66
The two primitives are independent: neither depends on the other, and either
could land first. They are presented together because they share one motivation
(making deterministic mutation pipelines practical), are designed to compose,
and are each individually small. Because they are independent, bundling them
imposes no merge-order constraint between implementation PRs. An author who
preferred could split them into two cross-linked standalone RFCs without
changing their substance; they are kept together here because they are easier to
evaluate as two halves of the same problem.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

two halves of the same problem

You should explain more about why they are so deeply correlated. From what I see you never mention how one complements the other, or how a certain goal cannot be reached without both.


### AI assistance

AI was used to make holistic design decisions.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Which decisions?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm kind of confused about that – shouldn't language models be even worse for large-scale design decisions than for generating language? And isn't the cost of a bad large-scale design decision higher than for a bad implementation too, making it even more important to actually think things through?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is just a courtesy to @alice-i-cecile state that I used AI to learn and make informed decisions.

It wasn't really the main driver. I wanted to avoid spending too much time on this topic. Would appreciate if we can focus on the merits of the RFC

Observers for the same event can be ordered using the same vocabulary as
systems.

2. Cross-bucket observer ordering.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What does "cross-bucket" mean?

Comment on lines +176 to +182
5. A safe write gate for restricted components.

Components that opt into restricted access cannot be mutated through ordinary
safe in-place mutable ECS access such as [`Query`][query]`<&mut T>`,
[`Query`][query]`<`[`Mut`][mut]`<T>>`,
typed world/entity mutable access, or safe untyped mutable access by
[`ComponentId`][component-id].
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This only explains the limitations. What are the advantages that you gain by using restricted access that you cannot have otherwise? What do you gain by preventing the use of Query<&mut T>? I don't see this mentioned anywhere else in the RFC, and that's the most critical motivation for the feature.


7. A narrow engine surface that libraries can build on.

Bevy provides ordering and access control. Library policy remains downstream.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What is a "library policy" here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Comment on lines +329 to +354
A downstream library can wrap this further. In the example below `MutationLog` and
`HealthChanged` are illustrative application-owned types, not Bevy APIs:

```rust
fn apply_logged_damage(
damaged: Query<(Entity, &IncomingDamage)>,
mut health: RestrictedMut<Health>,
mut log: ResMut<MutationLog>,
) {
for (entity, damage) in &damaged {
let result = health.modify(entity, |health| {
let before = health.current;
health.current = health.current.saturating_sub(damage.amount);
before
});

if let Ok(before) = result {
log.push(HealthChanged {
entity,
before,
after_damage: damage.amount,
});
}
}
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What is the advantage of having to use modify here? If another downstream crate did the following instead, wouldn't the log get out of sync?

fn apply_logged_damage_bad(
    damaged: Query<(Entity, &IncomingDamage)>,
    mut health: RestrictedMut<Health>,
) {
    for (entity, damage) in &damaged {
        let result = health.modify(entity, |health| {
            let before = health.current;
            health.current = health.current.saturating_sub(damage.amount);
            before
        });
    }
}

Or, what is wrong with the current approach of doing this?

fn apply_logged_damage(
    mut query: Query<(Entity, &mut Health, &IncomingDamage)>,
    mut log: ResMut<MutationLog>,
) {
    for (entity, mut health, damage) in &mut query {
        let before = health.current;
        health.current = health.current.saturating_sub(damage.amount);
        
        log.push(HealthChanged {
            entity,
            before,
            after_damage: damage.amount,
        });
    }
}

Comment on lines +404 to +417
The apply observer mutates through `RestrictedMut<RoundState>`:

```rust
fn apply_command(
commands_to_apply: Query<(Entity, &ValidatedCommand)>,
mut round_state: RestrictedMut<RoundState>,
) {
for (entity, command) in &commands_to_apply {
let _ = round_state.modify(entity, |state| {
state.turn += command.turn_delta;
});
}
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is already possible with mutable queries though.

Comment on lines +1397 to +1398
Component hooks observe structural changes, but they do not cover every in-place
mutation through safe mutable ECS access.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This sounds like you want to observer in-place mutations with observers, but the RFC doesn't actually do that.

be added later if reflection-driven tooling has a clear use case; see
[Reflection and scenes](#reflection-and-scenes) for the scene-loading implication.

[Unsafe APIs][unsafe-world-cell] remain unsafe escape hatches.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bevy_ecs' unsafe functions aren't escape hatches for component mutability. Trying to use them that way either fails or is unsound.

@laundmo
Copy link
Copy Markdown
Member

laundmo commented Jun 1, 2026

I would argue using LLMs for large-scale design decisions and writing RFC text is far beyond the simple autosuggest and other small-scale uses allowed by the AI Policy

Therefore, i would like to nominate this for closure. The label doesn't yet exist for this repo, so i have to state it like this.

Besides the AI Policy concerns, i've had a brief look at the contents and its proposing somewhat duplicated functionality without explaining the differences and besides that, i don't think this is the right way forward for how a solution for this problem should be designed.


Second, [component hooks][component-hooks] can observe structural changes such as add, insert,
discard, remove, and despawn, but they do not run for every in-place mutation
made through safe mutable ECS access. A downstream library can ask users to
Copy link
Copy Markdown
Member

@janhohenheim janhohenheim Jun 1, 2026

Choose a reason for hiding this comment

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

Immutable components serve this gap currently. You can reinsert an immutable component with a new value to get a de-facto mutation while triggering hooks / observers.

@alice-i-cecile
Copy link
Copy Markdown
Member

Independent of the AI policy concerns, I don't think that this is refined enough to be worth serious consideration at this point.

I'm also going to archive this repository: RFCs are effectively dead as a process.

@caniko
Copy link
Copy Markdown
Author

caniko commented Jun 1, 2026

It is quite useful because it shows what people think about the idea, which is very useful to refine or abandon it altogether.

I know that I will need something like what I propose here for my needs when I build my games and neuro-simulator. Will make a PR coupled with downstream crates next time, it should paint a clearer picture.

I'm also going to archive this repository: RFCs are effectively dead as a process.

Please provide clarity in the future. You are just shutting down topics left and right. I understand the goal and purpose of keeping things tidy, but it is derailing discussions.

@alice-i-cecile
Copy link
Copy Markdown
Member

Sorry, let me be more blunt, as I have apparently been insufficiently clear. I have taken the time, as a subject matter expert, and given this a preliminary evaluation on its merits. This is questionable work, poorly motivated, confusingly written up, in likely violation of our AI policy and not worth further consideration. My colleagues have done a good job laying out the core problems here already.

The observer ordering work is worth doing, the restricted access work is not. These should not have been merged into one RFC, even though I can see how they relate.

I am not keeping things tidy, I am saying no, I do not want to do this. You are welcome to maintain your own fork of Bevy with these features, implemented to your preferences and on your timeline.

@caniko
Copy link
Copy Markdown
Author

caniko commented Jun 1, 2026

Why are you shutting down a discussion. I didn't even have time to make a simple retort.

Can you clarify the exact reasoning we can't have a discussion @alice-i-cecile, it is not in violation of any AI policy. I suggest you amend your policy to, people that use AI for any reason whatsoever are unwelcome. Cause that is what is going on here.

@urben1680
Copy link
Copy Markdown

urben1680 commented Jun 1, 2026

Some advice I want to repost here is that you really should join the community on Discord more and talk about your ideas with other contributors first before putting that much work into upstreaming stuff like this. We don't bite and can learn from each other without the immediate need of Alice or someone else of the org to do a final decision.

@caniko
Copy link
Copy Markdown
Author

caniko commented Jun 1, 2026

I appreciate the deescalation, but I am not sure I understand what the difference between having an RFC and dicussing on Discord. Everybody showed up, we were having a great discussion, maybe a bit too much focus on this AI policy thing, but still a lot of great feedback. Then @alice-i-cecile shows up, and shuts it all down. I really don't understand. It has no benefit whatsover.

The RFC was even a draft, I didn't want the full force of the core authors to apply vigor to my work. Not yet, atleast. I wanted to start as a discussion, and morph it into a full RFC, and mark as ready, when it was ready.

I don't think my time was wasted, writing an RFC is a good exercise.

@laundmo
Copy link
Copy Markdown
Member

laundmo commented Jun 1, 2026

Alice shut it down because bevy as a project has not used RFCs in a few years. The fact this repo was left unarchived was a mistake. The current process for proposals like this is design docs, usually produced and discussed by working groups. Alternatively, for smaller features, the process is to discuss in the original feature request issue and in the relevant discord dev channel until a design has reached consensus.

@caniko
Copy link
Copy Markdown
Author

caniko commented Jun 1, 2026

... I am saying no, I do not want to do this ...

She shut it down.

@alice-i-cecile
Copy link
Copy Markdown
Member

Why are you shutting down a discussion. I didn't even have time to make a simple retort.

Maintainer and contributor time is precious. Stop wasting it. The next step here is a ban.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants