Deterministic mutation hooks for ECS#87
Conversation
Two bevy_ecs primitives for deterministic mutation pipelines: ObserverSet (ordered observer dispatch) and restricted component access (#[component(restricted)] + RestrictedMut<T>).
|
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? |
|
It is not clear to me how 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. |
| 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
| 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]. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
I suggest you read: https://bevy.org/learn/contribute/project-information/project-goals/
| 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, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| ``` |
There was a problem hiding this comment.
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,
});
}
}| 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; | ||
| }); | ||
| } | ||
| } | ||
| ``` |
There was a problem hiding this comment.
This is already possible with mutable queries though.
| Component hooks observe structural changes, but they do not cover every in-place | ||
| mutation through safe mutable ECS access. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
bevy_ecs' unsafe functions aren't escape hatches for component mutability. Trying to use them that way either fails or is unsound.
|
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 |
There was a problem hiding this comment.
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.
|
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. |
|
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.
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. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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. |
She shut it down. |
Maintainer and contributor time is precious. Stop wasting it. The next step here is a ban. |
RFC: Deterministic ECS mutation primitives
This RFC proposes two
bevy_ecsprimitives for deterministic mutation pipelines:ObserverSetand ordered observer dispatch, matching the familiar.before/.after/.in_set/.chain()scheduling vocabulary.#[component(restricted)]andRestrictedMut<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#24370is 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:
#[derive(Component)] #[component(restricted)]as recommended here, or the prototype's#[derive(RestrictedAccess)];RestrictedMut<T>::get_mutshould be included in v1 or whether the API should start with closure-stylemodifyplus iteration;ComponentIdpaths returningMutUntyped.