Skip to content

feat: stabilise script systems (lifecycle, errors, soundness) - #647

Open
makspll wants to merge 7 commits into
mainfrom
agent/script-systems-stabilise
Open

makspll wants to merge 7 commits into
mainfrom
agent/script-systems-stabilise

Conversation

@makspll

@makspll makspll commented Oct 22, 2026

Copy link
Copy Markdown
Owner

First of the stacked script system PRs (stabilise → access → scheduling → queries → perf).

Lifecycle

  • A script system is identified by (attachment, callback label). Adding it again replaces the old one, with a warn!.
  • Systems are removed when their script unloads or reloads. They are removed from remove_script_systems_when_unloaded observers, which always run, independent of the unload callback setting.
  • ScriptSystemSet(u32) holds a slot in the new ScriptSystems resource. Slots are recycled, so the interning leak (one per distinct set value) is bounded by the peak number of live script systems.
  • Sets are never removed from schedules, only their systems (RemoveSystemsOnlyAllowBreakages). In Bevy 0.20, SystemSets::remove keeps the set in its ids map, so adding to a removed set panics. A slot that a live script system orders against is not recycled.
  • Changes to running schedules (e.g. PreUpdate, where the pipeline runs) are queued, together with later changes to the same schedule, and applied by apply_script_system_changes in First and Last. Removed systems stop running right away through an alive flag. When the add is deferred, world.add_system returns a ReflectSystem without a node id.

Robustness

  • Errors are sent as ScriptErrorMessages (collected in ScriptSystems, sent by the same system). Invalid params are reported once, after which the system no longer runs; previously the init errors were popped once per frame and the system then ran anyway.
  • No panics:
    • an uninitialised system, or one run in another world, is Skipped
    • scope_schedule's assert! is now an error, and the schedule is re-inserted even if Schedules was removed
  • The ScriptContexts read lock is released before P::handle. Previously a callback that loaded or unloaded a script could deadlock.

Soundness

Bevy schedules a script system by its FilteredAccess, so query(C):with(A) runs in parallel with Query<&mut C, Without<A>>. The guard used to grant C on every entity, so world.get_component(e_without_a, C) gave aliased mutable access.

The new ArchetypeFilter (in bevy_mod_scripting_world) restricts each component to the archetypes matched by the queries that declare it. The check happens in reflect_(mut_)unsafe. Its module docs cover why archetypes and not tables, why it is sound for every filter, and why the filter can't go stale.

The new test system_cannot_access_component_outside_query_archetypes.lua fails with the check disabled. Benches (field get/set, component/resource access/get, query/100) stay within noise against main, but they run in exclusive contexts, so the filtered path isn't benched.

Tests

  • Rust: replace, removal + slot recycling, deferred changes to running schedules, error messages.
  • Lua: replacement and the archetype guard. system_with_parameters.lua now records finished: its assertions only got logged before, and its _1 tuple-field access had never worked.

🤖 Generated with Claude Code

- script systems are identified by (attachment, callback label): adding one
  again replaces it, and they are removed when their script unloads or reloads
- system sets hold recycled slots, bounding the interning leak; sets are never
  removed from schedules (bevy 0.20 panics when a removed set is reused)
- changes to running schedules (i.e. PreUpdate) are queued and applied in
  First/Last; removed systems stop running immediately
- script system errors are sent as ScriptErrorMessages, invalid parameters are
  reported once instead of being skipped silently
- no panics: uninitialized/cross-world runs are skipped, scope_schedule keeps
  the schedule on errors, the contexts lock is released before callbacks
- soundness: non-exclusive guards restrict component access to the archetypes
  matched by the declaring queries (ArchetypeFilter), bevy runs filtered
  queries in parallel with disjoint ones
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

ProjectBevy Scripting
Branchagent/script-systems-stabilise
Testbedlinux-gha

⚠️ WARNING: Truncated view!

The full continuous benchmarking report exceeds the maximum length allowed on this platform.

🐰 View full continuous benchmarking report in Bencher

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant