Skip to content

Add SystemIdTemplate to allow one-shot system in bsn! - #24026

Open
hxYuki wants to merge 13 commits into
bevyengine:mainfrom
hxYuki:system-id-template
Open

Add SystemIdTemplate to allow one-shot system in bsn!#24026
hxYuki wants to merge 13 commits into
bevyengine:mainfrom
hxYuki:system-id-template

Conversation

@hxYuki

@hxYuki hxYuki commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Objective

Solution

  • Add SystemIdTemplate

    pub struct SystemIdTemplate(Arc<Mutex<Option<SystemOrId>>>);
    
    enum SystemOrId {
        BoxedSystem(BoxedSystem<(), ()>),
        SystemId(SystemId<(), ()>),
    }
  • Add a helper function system_value<M> to convert a system to SystemIdTemplate

  • When a SystemIdTemplate is built, record its TypeId for the SystemId in HashMap and spawn a "relationship pair" between the template entity and the registered system entity.

  • Each cloned template built will get the inserted SystemId.

  • Each new built template refers to same system will get the SystemId by system type.

  • When a template item despawns, despawn all its "relationship pairs" entities, which triggers a hook on the relationship component to check if the system entity is still referenced. If not, unregister it and remove the record.

Testing

codes
fn main() {
    fn run_callbacks(mut commands: Commands, callbacks: Query<(Entity,&Callback)>) {
        callbacks.iter().for_each(|(entity, callback)| {
            commands.run_system(callback.0);

            // It will print "1,2,1,2" for the scene entity is despawned after
            // called. The system is unregistered then re-registered.
            // Comment this line you will get "1,2,3,4,5,6".
            commands.entity(entity).despawn();
        });
    }
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, (callback_scene.spawn(), run_callbacks, callback_scene.spawn(), run_callbacks).chain())
        .run();
}

#[derive(Component, FromTemplate)]
struct Callback(bevy_ecs::system::SystemId);

#[derive(Component, FromTemplate)]
struct SomeStruct{
        value: u32,
        callback: bevy_ecs::system::SystemId,
}
fn callback_scene() -> impl SceneList {
    bsn_list! {
        Name("template1") Callback(system_value(callback_system)),
        Name("template2") Callback(system_value(callback_system)) SomeStruct{value: 42, callback: system_value(another_callback_system)},
    }
}
fn callback_system(mut call_counter:Local<u32>){
    *call_counter += 1;
    println!("Hello from the system! Called: {}", *call_counter);
}

fn another_callback_system(mut call_counter:Local<u32>){
    *call_counter += 1;
    println!("Hello from another system! Called: {}", *call_counter);
}

Showcase

#[derive(Component, FromTemplate)]
struct Callback(SystemId);

fn callback_scene() -> impl SceneList {
    bsn_list! {
        Callback(system_value(|| {
            println!("Hello from the callback!");
        })),
        Callback(system_value(callback_system)),
    }
}

fn callback_system(){
    println!("Hello from the system!");
}

@github-actions

Copy link
Copy Markdown
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide, as well as our policy regarding AI usage, and we look forward to reviewing your pull request shortly ✨

@hxYuki

hxYuki commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

Not sure where to place these codes. I've searched impl FromTemplate for in repo and got only Entity here.

Comment thread crates/bevy_ecs/src/template.rs Outdated

impl Default for SystemIdTemplate {
fn default() -> Self {
Self::BoxedSystem(Arc::new(Mutex::new(Some(Box::new(IntoSystem::into_system(|| {}))))))

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.

What should be the default value for a SystemId?

Comment thread crates/bevy_ecs/src/template.rs Outdated
Comment thread crates/bevy_ecs/src/template.rs Outdated
Comment thread crates/bevy_ecs/src/template.rs Outdated
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events A-Scenes Composing and serializing ECS objects labels Apr 29, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS Apr 29, 2026
@alice-i-cecile alice-i-cecile added D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Apr 29, 2026
Comment thread crates/bevy_ecs/src/template.rs Outdated
Comment thread crates/bevy_ecs/src/template.rs Outdated
@hxYuki
hxYuki requested a review from SkiFire13 May 3, 2026 09:41

@SkiFire13 SkiFire13 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.

IMO this is becoming pretty complex and I'm kinda losing track of how everything fits together.

Comment thread crates/bevy_ecs/src/template.rs Outdated
Comment thread crates/bevy_ecs/src/template.rs Outdated
Comment thread crates/bevy_ecs/src/template.rs
@hxYuki
hxYuki marked this pull request as ready for review May 4, 2026 07:38
@hxYuki
hxYuki requested a review from SkiFire13 May 4, 2026 09:14
@cart cart closed this May 5, 2026
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in ECS 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 ECS May 5, 2026
@hxYuki
hxYuki force-pushed the system-id-template branch from 343586a to 7f130f0 Compare May 8, 2026 08:23
@hxYuki
hxYuki force-pushed the system-id-template branch from 7f130f0 to 83ec72e Compare May 12, 2026 10:29
mockersf added a commit to mockersf/bevy that referenced this pull request May 22, 2026
# Objective

bevyengine#24087 introduces scene templating for `SystemId`s, however it can
result in a memory leak if a scene is re-constructed multiple times:

bevyengine#24087 (comment)
> This was proposed basically 1:1 in bevyengine#24026 (this was later changed
though). The issue is that it's unclear who owns these systems, that is
who is responsible for unregistering them once they are no longer
needed. Given that recreating the template will spawn the system again
this basically becomes a memory leak.

bevyengine#24087 (comment)
> > Hm, are you sure this is the case even tho in `build_template` it
only registers the system the first time its called, switching over to
storing the SystemId after the first call?
>
> If you recreate the template (e.g. you call `my_scene()` again) then
you will create a new instance of the system. And since the system is
not scoped to the scene once the scene is despawned the system entity
will be leaked.

Essentially, we need a way to connect the lifetime of the registered
system to the lifetime of the scene.

## Solution

This is a purely additive / opt-in / backwards-compatible version of
bevyengine#24114

Introducing: `SystemHandle`s

```rust
pub enum SystemHandle<I: SystemInput = (), O = ()> {
    /// A strong handle keeps the system entity alive as long as the handle
    /// (and any clones of it) exist.
    Strong(Arc<StrongSystemHandle>),
    /// A weak handle does not keep the system entity alive.
    Weak(SystemId<I, O>),
}

pub struct StrongSystemHandle {
    entity: Entity,
    drop_queue: Arc<ConcurrentQueue<<Entity>>,
}
```

Similar to `bevy_asset::Handle`s,`SystemHandle`'s custom `Drop`
implementation enqueues the registered system entity into a concurrent
queue. The system `despawn_unused_registered_systems` pulls from the
other end of this queue and despawns the registered system entities.

`World::register_tracked_system` and
`World::register_tracked_boxed_system` are the only functions that
return `SystemHandle`s.

## Testing

- Added a test to ensure that `despawn_unused_registered_systems` does
its job
- Added a test to ensure that the default app will automatically call
`despawn_unused_registered_systems`

## Future work

- bevyengine#24087 will use this PR as a base

---------

Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
mockersf added a commit that referenced this pull request Jun 10, 2026
# Objective

#24087 introduces scene templating for `SystemId`s, however it can
result in a memory leak if a scene is re-constructed multiple times:

#24087 (comment)
> This was proposed basically 1:1 in #24026 (this was later changed
though). The issue is that it's unclear who owns these systems, that is
who is responsible for unregistering them once they are no longer
needed. Given that recreating the template will spawn the system again
this basically becomes a memory leak.

#24087 (comment)
> > Hm, are you sure this is the case even tho in `build_template` it
only registers the system the first time its called, switching over to
storing the SystemId after the first call?
>
> If you recreate the template (e.g. you call `my_scene()` again) then
you will create a new instance of the system. And since the system is
not scoped to the scene once the scene is despawned the system entity
will be leaked.

Essentially, we need a way to connect the lifetime of the registered
system to the lifetime of the scene.

## Solution

This is a purely additive / opt-in / backwards-compatible version of
#24114

Introducing: `SystemHandle`s

```rust
pub enum SystemHandle<I: SystemInput = (), O = ()> {
    /// A strong handle keeps the system entity alive as long as the handle
    /// (and any clones of it) exist.
    Strong(Arc<StrongSystemHandle>),
    /// A weak handle does not keep the system entity alive.
    Weak(SystemId<I, O>),
}

pub struct StrongSystemHandle {
    entity: Entity,
    drop_queue: Arc<ConcurrentQueue<<Entity>>,
}
```

Similar to `bevy_asset::Handle`s,`SystemHandle`'s custom `Drop`
implementation enqueues the registered system entity into a concurrent
queue. The system `despawn_unused_registered_systems` pulls from the
other end of this queue and despawns the registered system entities.

`World::register_tracked_system` and
`World::register_tracked_boxed_system` are the only functions that
return `SystemHandle`s.

## Testing

- Added a test to ensure that `despawn_unused_registered_systems` does
its job
- Added a test to ensure that the default app will automatically call
`despawn_unused_registered_systems`

## Future work

- #24087 will use this PR as a base

---------

Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
@JaySpruce JaySpruce added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events A-Scenes Composing and serializing ECS objects 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

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

Allow registering one-shot system in bsn!

5 participants