-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
System & frame stepping support #8063
Conversation
This is very much a short-cut proof-of-concept. Breakout happens to all of the gameplay in the `FixedUpdate` schedule, so I didn't have to tag every input & render system in bevy as `ignore_stepping()`.
Welcome, new contributor! Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨ |
@@ -40,6 +40,7 @@ pub struct SystemConfig { | |||
pub(super) system: BoxedSystem, | |||
pub(super) graph_info: GraphInfo, | |||
pub(super) conditions: Vec<BoxedCondition>, | |||
pub(super) ignore_stepping: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would probably be clearer as a SteppingBehavior
enum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the rust idiom here?
enum SteppingBehavior {
Permit,
Ignore,
}
// or
enum SteppingBehavior {
PermitStepping,
IgnoreStepping,
}
The actual code has comments and defaults, but I'm not sure about naming convention here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaning towards the second to permit:
use config::SteppingBehavior::*;
let stepping = match stepping_behavior {
PermitStepping => true,
IgnoreStepping => false,
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also looking for better names here.
I rejected Enable
/Disable
because they conflict with enabling/disabling on the Schedule
. I want to avoid "oh, you have to enable stepping both on the system and the schedule".
Observe
/Ignore
is an option, but not much better than Permit
/Ignore
.
Observe
/Disregard
?
Allow
/Deny
-- Deny
feels too much like it could stop stepping from working at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
enum Stepping {
Incremental,
Continual,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just came across Exempt
, but at the same time, that's a real weird english word, and doesn't have a clear antonym. Perhaps Observe
/Exempt
?
Example |
Moved the stepping control & UI code out into a module, and loading it back into breakout as a plugin. The code should work fine for any of the other game examples. Will get to that eventually. I had to add a bunch of helper methods to `Schedule` to get the information needed for the UI. Need to clean those up still. Also, there's a bug in the mapping from `SystemConfig::stepping_behavior`, through `ScheduleGraph::system_stepping_enabled`, into `SystemsSchedule::systems_with_stepping_enabled`. When I started marking bevy systems as `ignore_stepping()`, different systems were ignoring stepping in the UI. It may also be a bug in my quickly thrown together helpers in `Schedule`.
I wasn't using the correct index when transferring the stepping enabled systems from `ScheduleGraph` to `SystemSchedule`. That's fixed.
I went through every `impl Plugin` looking for calls to `add_systems()`, and added `ignore_stepping()` to all of them. It feels a little dirty, so any suggestions of a better way to disperse this would be appreciated. I just don't know of a better approach than scattering these throughout the code. I also broke these changes into a single commit so it's easy to see all of them at once.
Example |
@@ -78,7 +81,8 @@ impl Plugin for Core3dPlugin { | |||
sort_phase_system::<Opaque3d>.in_set(RenderSet::PhaseSort), | |||
sort_phase_system::<AlphaMask3d>.in_set(RenderSet::PhaseSort), | |||
sort_phase_system::<Transparent3d>.in_set(RenderSet::PhaseSort), | |||
), | |||
) | |||
.ignore_stepping(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really understand why rustfmt
is indenting this, but it happens everywhere I added ignore_stepping()
to a tuple.
First: this is really cool and something I'd like to include in Bevy. I haven't done a full review yet, but after a first pass my biggest concern is the prevalence of |
TL;DR: To implement stepping, there must be some complexity added. For the
@cart Some complexity is inherent in the nature of system-based stepping, but First off, I went nuts with Critical Systems for a Responsive ApplicationFor stepping to be a usable feature, there exists a subset of systems Edit I managed to forget about a bevy pattern where systems run Schedules, such as There must be some way to track which systems are critical to a responsive Outside of those What is "safe"? And making "the right call" for your system(Note: This section is written assuming that at some point we make the change The vast majority of systems will never have to be considered in the context of For rendering systems, there's no complexity here as render will always ignore For input systems, there is a little complexity; Is this system required for Edit If a system calls
|
After implementing pause functionality in my game, it's struck me that, by and large, I want to group this stepping (and pause) behavior based on the data that the components access. If we had automatically lazily generated system sets on the basis of access (#7857), I think that maintaining this distinction might be much less onerous. For example, everything that writes to a Window or Input should probably be ignored by default. Not a complete fix, but perhaps a useful direction. We could also configure a default on a per-schedule basis? |
This is interesting, but I'm curious if it's just moving the I see two categories here: what system should always be run ( BUT! Oh, does this mean there's a way to tell from the |
Yes, that should be possible on the basis of the |
To help illustrate a direction I think we should be headed in, I put together a simple draft PR: #8168. |
@alice-i-cecile I'm not seeing how to make this work. I've set up the following test to print the struct TestEvent;
fn event_system(mut reader: EventReader<TestEvent>) {
for _ in reader.iter() {}
}
#[test]
fn detect_event_system() {
let system = IntoSystem::into_system(event_system);
println!("system.component_access: {:#?}", system.component_access());
println!(
"system.archetype_component_access: {:#?}",
system.archetype_component_access()
);
assert!(false);
} The output shows empty system.component_access: Access {
read_and_writes: [],
writes: [],
reads_all: false,
}
system.archetype_component_access: Access {
read_and_writes: [],
writes: [],
reads_all: false,
} Is there some other mechanism to detect a system takes an |
The issue is that you aren't initializing the system. A system's access sets will be empty until you call |
@JoJoJet Thank you! That makes more sense. |
Followup on the detecting event reader systems; I got it working, but it requires /// helper function to determine if a system reads events
#[allow(dead_code)]
fn system_reads_events(
system: &dyn System<In = (), Out = ()>,
world: &crate::world::World,
) -> bool {
for id in system.component_access().reads() {
if world
.components()
.get_name(id)
.unwrap()
.starts_with("bevy_ecs::event::Events<")
{
return true;
}
}
false
}
struct TestEvent;
fn read_event_system(mut reader: EventReader<TestEvent>) {
for _ in reader.iter() {}
}
fn write_event_system(mut writer: EventWriter<TestEvent>) {
writer.send(TestEvent);
}
#[test]
fn verify_system_reads_events() {
let mut world = World::new();
let mut reader = IntoSystem::into_system(read_event_system);
reader.initialize(&mut world);
let mut writer = IntoSystem::into_system(write_event_system);
writer.initialize(&mut world);
assert!(system_reads_events(&reader, &world));
assert!(!system_reads_events(&writer, &world));
} |
Closing in favor of #8453. |
# Objective Add interactive system debugging capabilities to bevy, providing step/break/continue style capabilities to running system schedules. * Original implementation: #8063 - `ignore_stepping()` everywhere was too much complexity * Schedule-config & Resource discussion: #8168 - Decided on selective adding of Schedules & Resource-based control ## Solution Created `Stepping` Resource. This resource can be used to enable stepping on a per-schedule basis. Systems within schedules can be individually configured to: * AlwaysRun: Ignore any stepping state and run every frame * NeverRun: Never run while stepping is enabled - this allows for disabling of systems while debugging * Break: If we're running the full frame, stop before this system is run Stepping provides two modes of execution that reflect traditional debuggers: * Step-based: Only execute one system at a time * Continue/Break: Run all systems, but stop before running a system marked as Break ### Demo https://user-images.githubusercontent.com/857742/233630981-99f3bbda-9ca6-4cc4-a00f-171c4946dc47.mov Breakout has been modified to use Stepping. The game runs normally for a couple of seconds, then stepping is enabled and the game appears to pause. A list of Schedules & Systems appears with a cursor at the first System in the list. The demo then steps forward full frames using the spacebar until the ball is about to hit a brick. Then we step system by system as the ball impacts a brick, showing the cursor moving through the individual systems. Finally the demo switches back to frame stepping as the ball changes course. ### Limitations Due to architectural constraints in bevy, there are some cases systems stepping will not function as a user would expect. #### Event-driven systems Stepping does not support systems that are driven by `Event`s as events are flushed after 1-2 frames. Although game systems are not running while stepping, ignored systems are still running every frame, so events will be flushed. This presents to the user as stepping the event-driven system never executes the system. It does execute, but the events have already been flushed. This can be resolved by changing event handling to use a buffer for events, and only dropping an event once all readers have read it. The work-around to allow these systems to properly execute during stepping is to have them ignore stepping: `app.add_systems(event_driven_system.ignore_stepping())`. This was done in the breakout example to ensure sound played even while stepping. #### Conditional Systems When a system is stepped, it is given an opportunity to run. If the conditions of the system say it should not run, it will not. Similar to Event-driven systems, if a system is conditional, and that condition is only true for a very small time window, then stepping the system may not execute the system. This includes depending on any sort of external clock. This exhibits to the user as the system not always running when it is stepped. A solution to this limitation is to ensure any conditions are consistent while stepping is enabled. For example, all systems that modify any state the condition uses should also enable stepping. #### State-transition Systems Stepping is configured on the per-`Schedule` level, requiring the user to have a `ScheduleLabel`. To support state-transition systems, bevy generates needed schedules dynamically. Currently it’s very difficult (if not impossible, I haven’t verified) for the user to get the labels for these schedules. Without ready access to the dynamically generated schedules, and a resolution for the `Event` lifetime, **stepping of the state-transition systems is not supported** --- ## Changelog - `Schedule::run()` updated to consult `Stepping` Resource to determine which Systems to run each frame - Added `Schedule.label` as a `BoxedSystemLabel`, along with supporting `Schedule::set_label()` and `Schedule::label()` methods - `Stepping` needed to know which `Schedule` was running, and prior to this PR, `Schedule` didn't track its own label - Would have preferred to add `Schedule::with_label()` and remove `Schedule::new()`, but this PR touches enough already - Added calls to `Schedule.set_label()` to `App` and `World` as needed - Added `Stepping` resource - Added `Stepping::begin_frame()` system to `MainSchedulePlugin` - Run before `Main::run_main()` - Notifies any `Stepping` Resource a new render frame is starting ## Migration Guide - Add a call to `Schedule::set_label()` for any custom `Schedule` - This is only required if the `Schedule` will be stepped --------- Co-authored-by: Carter Anderson <[email protected]>
# Objective Add interactive system debugging capabilities to bevy, providing step/break/continue style capabilities to running system schedules. * Original implementation: bevyengine#8063 - `ignore_stepping()` everywhere was too much complexity * Schedule-config & Resource discussion: bevyengine#8168 - Decided on selective adding of Schedules & Resource-based control ## Solution Created `Stepping` Resource. This resource can be used to enable stepping on a per-schedule basis. Systems within schedules can be individually configured to: * AlwaysRun: Ignore any stepping state and run every frame * NeverRun: Never run while stepping is enabled - this allows for disabling of systems while debugging * Break: If we're running the full frame, stop before this system is run Stepping provides two modes of execution that reflect traditional debuggers: * Step-based: Only execute one system at a time * Continue/Break: Run all systems, but stop before running a system marked as Break ### Demo https://user-images.githubusercontent.com/857742/233630981-99f3bbda-9ca6-4cc4-a00f-171c4946dc47.mov Breakout has been modified to use Stepping. The game runs normally for a couple of seconds, then stepping is enabled and the game appears to pause. A list of Schedules & Systems appears with a cursor at the first System in the list. The demo then steps forward full frames using the spacebar until the ball is about to hit a brick. Then we step system by system as the ball impacts a brick, showing the cursor moving through the individual systems. Finally the demo switches back to frame stepping as the ball changes course. ### Limitations Due to architectural constraints in bevy, there are some cases systems stepping will not function as a user would expect. #### Event-driven systems Stepping does not support systems that are driven by `Event`s as events are flushed after 1-2 frames. Although game systems are not running while stepping, ignored systems are still running every frame, so events will be flushed. This presents to the user as stepping the event-driven system never executes the system. It does execute, but the events have already been flushed. This can be resolved by changing event handling to use a buffer for events, and only dropping an event once all readers have read it. The work-around to allow these systems to properly execute during stepping is to have them ignore stepping: `app.add_systems(event_driven_system.ignore_stepping())`. This was done in the breakout example to ensure sound played even while stepping. #### Conditional Systems When a system is stepped, it is given an opportunity to run. If the conditions of the system say it should not run, it will not. Similar to Event-driven systems, if a system is conditional, and that condition is only true for a very small time window, then stepping the system may not execute the system. This includes depending on any sort of external clock. This exhibits to the user as the system not always running when it is stepped. A solution to this limitation is to ensure any conditions are consistent while stepping is enabled. For example, all systems that modify any state the condition uses should also enable stepping. #### State-transition Systems Stepping is configured on the per-`Schedule` level, requiring the user to have a `ScheduleLabel`. To support state-transition systems, bevy generates needed schedules dynamically. Currently it’s very difficult (if not impossible, I haven’t verified) for the user to get the labels for these schedules. Without ready access to the dynamically generated schedules, and a resolution for the `Event` lifetime, **stepping of the state-transition systems is not supported** --- ## Changelog - `Schedule::run()` updated to consult `Stepping` Resource to determine which Systems to run each frame - Added `Schedule.label` as a `BoxedSystemLabel`, along with supporting `Schedule::set_label()` and `Schedule::label()` methods - `Stepping` needed to know which `Schedule` was running, and prior to this PR, `Schedule` didn't track its own label - Would have preferred to add `Schedule::with_label()` and remove `Schedule::new()`, but this PR touches enough already - Added calls to `Schedule.set_label()` to `App` and `World` as needed - Added `Stepping` resource - Added `Stepping::begin_frame()` system to `MainSchedulePlugin` - Run before `Main::run_main()` - Notifies any `Stepping` Resource a new render frame is starting ## Migration Guide - Add a call to `Schedule::set_label()` for any custom `Schedule` - This is only required if the `Schedule` will be stepped --------- Co-authored-by: Carter Anderson <[email protected]>
Objective
bevy-inspector-egui
to inspect & modify components between system executionsSolution
Schedule
, andSystemExecutor
to implement system & frame steppingSchedule
steppingSystems will permit stepping by default. This is more work within bevy, but allows crate users the most benefit by having it enabled by default.
Note: In the critical path (
SystemExecutor::run()
), when stepping is enabled, this PR only introduces one call, conditional check, and return.Limitations
As this is the initial implementation for this capability, and a need to limit scope of these changes, there are some limitations to system stepping.
Due to architectural constraints in bevy, there are some cases systems stepping will not function as a user would expect.
Event-driven systems
Stepping does not support systems that are driven by
Event
s as events are flushed after 1-2 frames. Although game systems are not running while stepping, ignored systems are still running every frame, so events will be flushed.This presents to the user as stepping the event-driven system never executes the system. It does execute, but the events have already been flushed.
This can be resolved by changing event handling to use a buffer for events, and only dropping an event once all readers have read it.
The work-around to allow these systems to properly execute during stepping is to have them ignore stepping:
app.add_systems(event_driven_system.ignore_stepping())
. This was done in the breakout example to ensure sound played even while stepping.Conditional Systems
When a system is stepped, it is given an opportunity to run. If the conditions of the system say it should not run, it will not.
Similar to Event-driven systems, if a system is conditional, and that condition is only true for a very small time window, then stepping the system may not execute the system. This includes depending on any sort of external clock.
This exhibits to the user as the system not always running when it is stepped.
A solution to this limitation is to ensure any conditions are consistent while stepping is enabled. For example, all systems that modify any state the condition uses should also enable stepping.
State-transition Systems
Stepping is controlled on the per-
Schedule
level, requiring the user to have aScheduleLabel
.To support state-transition systems, bevy generates needed schedules dynamically. Currently it’s very difficult (if not impossible, I haven’t verified) for the user to get the labels for these schedules.
Without ready access to the dynamically generated schedules, and a resolution for the
Event
lifetime, stepping of the state-transition systems is not supportedDemo
Updated demonstration of stepping with a UI in breakout example
Screen.Recording.2023-03-19.at.3.00.13.PM.mov
The grave key is used to enable stepping mode,
S
steps forward a single system,Space
steps forward a full frame. The video demonstrates walking through the systems as a collision occurs in breakout.A much better UI will be possible with the use of external crates such as egui.
Performance Details
As this change leverages the existing
completed_systems
list in theSystemExecutor
s, the addition of stepping has no discernible impact on performance.Measurement Methodology
I tweaked
FrameTimeDiagnosticsPlugin
to keep8000
fps samples, then rancargo run --example many_foxes --release
twice, lettingmany_foxes
run for 20 seconds. I ran this once with the hooks for stepping present in bothMultiThreadedExecutor
andSingleThreadExecutor
. For the second run, I commented out the hooks in both executors. Stepping was never enabled during either test run.The average FPS for the two runs after 20 seconds were within 1 fps of each other, around 126fps.
With a longer run (60 seconds), both binaries leveled out around 125fps.
Changelog
The key changes were made to
SystemSchedule
to track stepping state, and return a list of systems to be skipped inSystemExecutor::run()
. All system executors were updated to apply the skip list fromSystemSchedule
.A new event type,
ScheduleEvent
, was added to control stepping behavior from within systems. This was needed as the currently running schedule is not accessible in theSchedules
resource, and we don't want stepping changes to apply mid-frame.App::update()
callsSchedule::handle_event()
for each event after all schedules have been run.Added
example/games/stepping.rs
, a new plugin to control stepping and display a UI for example games. It has been integrated into both breakout and alien_cake_addict.Added a number of helper functions to
Schedule
to expose needed data for the example UI.Marked every existing system in bevy with
ignore_stepping()
in theiradd_systems()
calls. This should ensure bevy is still fully functional (input/ui/render) when stepping is enabled.Migration Guide
If a new system is added to one of bevy's plugins, ensure
.ignore_stepping()
is added. Example: