Skip to content

SystemId scene templating - #24087

Merged
alice-i-cecile merged 2 commits into
bevyengine:mainfrom
ItsDoot:ecs/systemidtemplate
May 31, 2026
Merged

SystemId scene templating#24087
alice-i-cecile merged 2 commits into
bevyengine:mainfrom
ItsDoot:ecs/systemidtemplate

Conversation

@ItsDoot

@ItsDoot ItsDoot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Objective

I have a bunch of Bundle-style code that I want to replace with the new bsn! Scene-style macro:

pub fn rest_ui(/* system params */) {
    // my ui system...
}

// From
pub fn rest(mut commands: Commands) -> impl Bundle {
    (
        Rest,
        Name::new("Rest"),
        Activity {
            render: commands.register_system(rest_ui),
        },
    )
}

// To (ideally)
pub fn rest() -> impl Scene {
    bsn! {
        Rest
        Name("Rest")
        Activity {
            render: rest_ui
        }
    }
}

Solution

This solution is more inspired by how HandleTemplate works.

  1. Added SystemIdTemplate; it stores either a SystemId or a Arc<Mutex<Either<SystemId, Box<dyn System>>>>
  2. Added a system_value function for wrapping system functions (see Future Work for potentially removing the need)

Testing

  • Added a unit test for SystemIdTemplate.
  • Added to the callbacks example demonstrating how to spawn SystemIds via BSN scenes.

Future work


Showcase

You can now spawn components containing SystemIds via bsn! macros:

#[derive(Component, FromTemplate)]
struct Callback {
    system_id: SystemId<(), ()>,
}

fn my_scene() -> impl Scene {
    bsn! {
        Callback {
            system_id: system_value(|| {
                println!("This is a callback spawned via a scene.");
            })
        }
    }
}

@ItsDoot ItsDoot added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Scenes Composing and serializing ECS objects 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 labels May 2, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS May 2, 2026
@laundmo

laundmo commented May 3, 2026

Copy link
Copy Markdown
Member

Glad to see my idea from #24072 (comment) had some merit! I won't approve yet, since i've only looked through it on my phone, but it already seems far simpler.

@SkiFire13

Copy link
Copy Markdown
Contributor

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.

@laundmo

laundmo commented May 3, 2026

Copy link
Copy Markdown
Member

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.

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?

@SkiFire13

Copy link
Copy Markdown
Contributor

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.

@chescock

chescock commented May 3, 2026

Copy link
Copy Markdown
Contributor

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.

Would it work to use register_system_cached instead of register_system here?

Then it's not a leak because we could always use the SystemId again, even by recreating the scene. It also avoids some edge cases if you try to use a scene with multiple worlds. And you could probably require the IntoSystem to be Clone and avoid needing Arcs, since ZST systems are usually Clone anyway.

If the systems need to capture values, then maybe those values could be stored as components on the parent entity instead of in the system? The system could take In<Entity> and then have a Query to look up the values. I don't know enough about the actual use cases to know how awkward that transformation would be.

@SkiFire13

Copy link
Copy Markdown
Contributor

Would it work to use register_system_cached instead of register_system here?

You cannot use register_system_cached with a BoxedSystem<I, O>. You need to have a concrete system type that is also a ZST. I don't see an easy way to do that here.

@chescock

chescock commented May 3, 2026

Copy link
Copy Markdown
Contributor

You cannot use register_system_cached with a BoxedSystem<I, O>. You need to have a concrete system type that is also a ZST. I don't see an easy way to do that here.

Right, you'd have to wrap the register call. It would be approximately Box::new(move |world| world.register_system_cached(s)). Except I think Box<dyn Fn> can't be Clone, so you'd need something like Box<dyn Template> with a separate template type.

@ItsDoot

ItsDoot commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

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.

I've come up with a bevy_asset::Handle style solution to this problem in #24114, dubbed SystemHandles.

@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
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>
@ItsDoot
ItsDoot force-pushed the ecs/systemidtemplate branch from a26da4f to 9993182 Compare May 29, 2026 07:24
@ItsDoot

ItsDoot commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Was going to just do a git merge but it turned into a headache so I went with a clean rebase on top of current main.

Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
Comment thread crates/bevy_ecs/src/system/system_registry.rs
Comment thread crates/bevy_ecs/src/system/system_registry.rs
Comment thread crates/bevy_ecs/src/system/system_registry.rs
@alice-i-cecile alice-i-cecile 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 May 31, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue May 31, 2026
Merged via the queue into bevyengine:main with commit f4f9aab May 31, 2026
40 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in ECS May 31, 2026
@alice-i-cecile alice-i-cecile added this to the 0.19 milestone Jun 1, 2026
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>
mockersf pushed a commit that referenced this pull request Jun 10, 2026
# Objective

- Simplified alternative to #24072

I have a bunch of `Bundle`-style code that I want to replace with the
new `bsn!` Scene-style macro:

```rust
pub fn rest_ui(/* system params */) {
    // my ui system...
}

// From
pub fn rest(mut commands: Commands) -> impl Bundle {
    (
        Rest,
        Name::new("Rest"),
        Activity {
            render: commands.register_system(rest_ui),
        },
    )
}

// To (ideally)
pub fn rest() -> impl Scene {
    bsn! {
        Rest
        Name("Rest")
        Activity {
            render: rest_ui
        }
    }
}
```

## Solution

This solution is more inspired by how `HandleTemplate` works.

1. Added `SystemIdTemplate`; it stores either a `SystemId` or a
`Arc<Mutex<Either<SystemId, Box<dyn System>>>>`
2. Added a `system_value` function for wrapping system functions (see
Future Work for potentially removing the need)

## Testing

- Added a unit test for `SystemIdTemplate`.
- Added to the `callbacks` example demonstrating how to spawn
`SystemId`s via BSN scenes.

## Future work

- I believe we can remove the need for wrapping with `system_value` by
introducing [`SuperFrom`/`SuperInto` traits a la
Dioxus](https://docs.rs/dioxus-core/0.7.6/dioxus_core/trait.SuperFrom.html)
and using it in the `bsn!` macros in-place of the implicit `.into()`s.

---

## Showcase

You can now spawn components containing `SystemId`s via `bsn!` macros:

```rust
#[derive(Component, FromTemplate)]
struct Callback {
    system_id: SystemId<(), ()>,
}

fn my_scene() -> impl Scene {
    bsn! {
        Callback {
            system_id: system_value(|| {
                println!("This is a callback spawned via a scene.");
            })
        }
    }
}
```
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-Usability A targeted quality-of-life change that makes Bevy easier to use D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes 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: Done

Development

Successfully merging this pull request may close these issues.

6 participants