Skip to content

Commit 6a981aa

Browse files
alice-i-cecileMiniaczQdmyyychescock
authored
Define system param validation on a per-system parameter basis (#18504)
# Objective When introduced, `Single` was intended to simply be silently skipped, allowing for graceful and efficient handling of systems during invalid game states (such as when the player is dead). However, this also caused missing resources to *also* be silently skipped, leading to confusing and very hard to debug failures. In 0.15.1, this behavior was reverted to a panic, making missing resources easier to debug, but largely making `Single` (and `Populated`) worthless, as they would panic during expected game states. Ultimately, the consensus is that this behavior should differ on a per-system-param basis. However, there was no sensible way to *do* that before this PR. ## Solution Swap `SystemParam::validate_param` from a `bool` to: ```rust /// The outcome of system / system param validation, /// used by system executors to determine what to do with a system. pub enum ValidationOutcome { /// All system parameters were validated successfully and the system can be run. Valid, /// At least one system parameter failed validation, and an error must be handled. /// By default, this will result in1 a panic. See [crate::error] for more information. /// /// This is the default behavior, and is suitable for system params that should *always* be valid, /// either because sensible fallback behavior exists (like [`Query`] or because /// failures in validation should be considered a bug in the user's logic that must be immediately addressed (like [`Res`]). Invalid, /// At least one system parameter failed validation, but the system should be skipped due to [`ValidationBehavior::Skip`]. /// This is suitable for system params that are intended to only operate in certain application states, such as [`Single`]. Skipped, } ``` Then, inside of each `SystemParam` implementation, return either Valid, Invalid or Skipped. Currently, only `Single`, `Option<Single>` and `Populated` use the `Skipped` behavior. Other params (like resources) retain their current failing ## Testing Messed around with the fallible_params example. Added a pair of tests: one for panicking when resources are missing, and another for properly skipping `Single` and `Populated` system params. ## To do - [x] get #18454 merged - [x] fix the todo!() in the macro-powered tuple implementation (please help 🥺) - [x] test - [x] write a migration guide - [x] update the example comments ## Migration Guide Various system and system parameter validation methods (`SystemParam::validate_param`, `System::validate_param` and `System::validate_param_unsafe`) now return and accept a `ValidationOutcome` enum, rather than a `bool`. The previous `true` values map to `ValidationOutcome::Valid`, while `false` maps to `ValidationOutcome::Invalid`. However, if you wrote a custom schedule executor, you should now respect the new `ValidationOutcome::Skipped` parameter, skipping any systems whose validation was skipped. By contrast, `ValidationOutcome::Invalid` systems should also be skipped, but you should call the `default_error_handler` on them first, which by default will result in a panic. If you are implementing a custom `SystemParam`, you should consider whether failing system param validation is an error or an expected state, and choose between `Invalid` and `Skipped` accordingly. In Bevy itself, `Single` and `Populated` now once again skip the system when their conditions are not met. This is the 0.15.0 behavior, but stands in contrast to the 0.15.1 behavior, where they would panic. --------- Co-authored-by: MiniaczQ <[email protected]> Co-authored-by: Dmytro Banin <[email protected]> Co-authored-by: Chris Russell <[email protected]>
1 parent 206599a commit 6a981aa

20 files changed

+378
-183
lines changed

crates/bevy_ecs/macros/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
429429
state: &'s Self::State,
430430
system_meta: &#path::system::SystemMeta,
431431
world: #path::world::unsafe_world_cell::UnsafeWorldCell<'w>,
432-
) -> bool {
432+
) -> #path::system::ValidationOutcome {
433433
<(#(#tuple_types,)*) as #path::system::SystemParam>::validate_param(&state.state, system_meta, world)
434434
}
435435

crates/bevy_ecs/src/observer/runner.rs

+17-15
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
observer::{ObserverDescriptor, ObserverTrigger},
88
prelude::*,
99
query::DebugCheckedUnwrap,
10-
system::{IntoObserverSystem, ObserverSystem, SystemParamValidationError},
10+
system::{IntoObserverSystem, ObserverSystem, SystemParamValidationError, ValidationOutcome},
1111
world::DeferredWorld,
1212
};
1313
use bevy_ptr::PtrMut;
@@ -405,25 +405,27 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
405405
// - system is the same type erased system from above
406406
unsafe {
407407
(*system).update_archetype_component_access(world);
408-
if (*system).validate_param_unsafe(world) {
409-
if let Err(err) = (*system).run_unsafe(trigger, world) {
410-
error_handler(
411-
err,
412-
ErrorContext::Observer {
413-
name: (*system).name(),
414-
last_run: (*system).get_last_run(),
415-
},
416-
);
417-
};
418-
(*system).queue_deferred(world.into_deferred());
419-
} else {
420-
error_handler(
408+
match (*system).validate_param_unsafe(world) {
409+
ValidationOutcome::Valid => {
410+
if let Err(err) = (*system).run_unsafe(trigger, world) {
411+
error_handler(
412+
err,
413+
ErrorContext::Observer {
414+
name: (*system).name(),
415+
last_run: (*system).get_last_run(),
416+
},
417+
);
418+
};
419+
(*system).queue_deferred(world.into_deferred());
420+
}
421+
ValidationOutcome::Invalid => error_handler(
421422
SystemParamValidationError.into(),
422423
ErrorContext::Observer {
423424
name: (*system).name(),
424425
last_run: (*system).get_last_run(),
425426
},
426-
);
427+
),
428+
ValidationOutcome::Skipped => (),
427429
}
428430
}
429431
}

crates/bevy_ecs/src/schedule/executor/mod.rs

+75-27
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
prelude::{IntoSystemSet, SystemSet},
2121
query::Access,
2222
schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet},
23-
system::{ScheduleSystem, System, SystemIn},
23+
system::{ScheduleSystem, System, SystemIn, ValidationOutcome},
2424
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
2525
};
2626

@@ -221,10 +221,10 @@ impl System for ApplyDeferred {
221221

222222
fn queue_deferred(&mut self, _world: DeferredWorld) {}
223223

224-
unsafe fn validate_param_unsafe(&mut self, _world: UnsafeWorldCell) -> bool {
224+
unsafe fn validate_param_unsafe(&mut self, _world: UnsafeWorldCell) -> ValidationOutcome {
225225
// This system is always valid to run because it doesn't do anything,
226226
// and only used as a marker for the executor.
227-
true
227+
ValidationOutcome::Valid
228228
}
229229

230230
fn initialize(&mut self, _world: &mut World) {}
@@ -312,49 +312,97 @@ mod __rust_begin_short_backtrace {
312312
#[cfg(test)]
313313
mod tests {
314314
use crate::{
315-
prelude::{IntoScheduleConfigs, Resource, Schedule, SystemSet},
315+
prelude::{Component, Resource, Schedule},
316316
schedule::ExecutorKind,
317-
system::Commands,
317+
system::{Populated, Res, ResMut, Single},
318318
world::World,
319319
};
320320

321-
#[derive(Resource)]
322-
struct R1;
323-
324-
#[derive(Resource)]
325-
struct R2;
321+
#[derive(Component)]
322+
struct TestComponent;
326323

327324
const EXECUTORS: [ExecutorKind; 3] = [
328325
ExecutorKind::Simple,
329326
ExecutorKind::SingleThreaded,
330327
ExecutorKind::MultiThreaded,
331328
];
332329

330+
#[derive(Resource, Default)]
331+
struct TestState {
332+
populated_ran: bool,
333+
single_ran: bool,
334+
}
335+
336+
fn set_single_state(mut _single: Single<&TestComponent>, mut state: ResMut<TestState>) {
337+
state.single_ran = true;
338+
}
339+
340+
fn set_populated_state(
341+
mut _populated: Populated<&TestComponent>,
342+
mut state: ResMut<TestState>,
343+
) {
344+
state.populated_ran = true;
345+
}
346+
333347
#[test]
334-
fn invalid_system_param_skips() {
348+
#[expect(clippy::print_stdout, reason = "std and println are allowed in tests")]
349+
fn single_and_populated_skipped_and_run() {
335350
for executor in EXECUTORS {
336-
invalid_system_param_skips_core(executor);
351+
std::println!("Testing executor: {:?}", executor);
352+
353+
let mut world = World::new();
354+
world.init_resource::<TestState>();
355+
356+
let mut schedule = Schedule::default();
357+
schedule.set_executor_kind(executor);
358+
schedule.add_systems((set_single_state, set_populated_state));
359+
schedule.run(&mut world);
360+
361+
let state = world.get_resource::<TestState>().unwrap();
362+
assert!(!state.single_ran);
363+
assert!(!state.populated_ran);
364+
365+
world.spawn(TestComponent);
366+
367+
schedule.run(&mut world);
368+
let state = world.get_resource::<TestState>().unwrap();
369+
assert!(state.single_ran);
370+
assert!(state.populated_ran);
337371
}
338372
}
339373

340-
fn invalid_system_param_skips_core(executor: ExecutorKind) {
374+
fn look_for_missing_resource(_res: Res<TestState>) {}
375+
376+
#[test]
377+
#[should_panic]
378+
fn missing_resource_panics_simple() {
341379
let mut world = World::new();
342380
let mut schedule = Schedule::default();
343-
schedule.set_executor_kind(executor);
344-
schedule.add_systems(
345-
(
346-
// This system depends on a system that is always skipped.
347-
(|mut commands: Commands| {
348-
commands.insert_resource(R2);
349-
}),
350-
)
351-
.chain(),
352-
);
381+
382+
schedule.set_executor_kind(ExecutorKind::Simple);
383+
schedule.add_systems(look_for_missing_resource);
384+
schedule.run(&mut world);
385+
}
386+
387+
#[test]
388+
#[should_panic]
389+
fn missing_resource_panics_single_threaded() {
390+
let mut world = World::new();
391+
let mut schedule = Schedule::default();
392+
393+
schedule.set_executor_kind(ExecutorKind::SingleThreaded);
394+
schedule.add_systems(look_for_missing_resource);
353395
schedule.run(&mut world);
354-
assert!(world.get_resource::<R1>().is_none());
355-
assert!(world.get_resource::<R2>().is_some());
356396
}
357397

358-
#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
359-
struct S1;
398+
#[test]
399+
#[should_panic]
400+
fn missing_resource_panics_multi_threaded() {
401+
let mut world = World::new();
402+
let mut schedule = Schedule::default();
403+
404+
schedule.set_executor_kind(ExecutorKind::MultiThreaded);
405+
schedule.add_systems(look_for_missing_resource);
406+
schedule.run(&mut world);
407+
}
360408
}

crates/bevy_ecs/src/schedule/executor/multi_threaded.rs

+29-20
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
prelude::Resource,
1919
query::Access,
2020
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
21-
system::{ScheduleSystem, SystemParamValidationError},
21+
system::{ScheduleSystem, SystemParamValidationError, ValidationOutcome},
2222
world::{unsafe_world_cell::UnsafeWorldCell, World},
2323
};
2424

@@ -581,18 +581,24 @@ impl ExecutorState {
581581
// - The caller ensures that `world` has permission to read any data
582582
// required by the system.
583583
// - `update_archetype_component_access` has been called for system.
584-
let valid_params = unsafe { system.validate_param_unsafe(world) };
584+
let valid_params = match unsafe { system.validate_param_unsafe(world) } {
585+
ValidationOutcome::Valid => true,
586+
ValidationOutcome::Invalid => {
587+
error_handler(
588+
SystemParamValidationError.into(),
589+
ErrorContext::System {
590+
name: system.name(),
591+
last_run: system.get_last_run(),
592+
},
593+
);
594+
false
595+
}
596+
ValidationOutcome::Skipped => false,
597+
};
585598
if !valid_params {
586-
error_handler(
587-
SystemParamValidationError.into(),
588-
ErrorContext::System {
589-
name: system.name(),
590-
last_run: system.get_last_run(),
591-
},
592-
);
593-
594599
self.skipped_systems.insert(system_index);
595600
}
601+
596602
should_run &= valid_params;
597603
}
598604

@@ -789,16 +795,19 @@ unsafe fn evaluate_and_fold_conditions(
789795
// - The caller ensures that `world` has permission to read any data
790796
// required by the condition.
791797
// - `update_archetype_component_access` has been called for condition.
792-
if !unsafe { condition.validate_param_unsafe(world) } {
793-
error_handler(
794-
SystemParamValidationError.into(),
795-
ErrorContext::System {
796-
name: condition.name(),
797-
last_run: condition.get_last_run(),
798-
},
799-
);
800-
801-
return false;
798+
match unsafe { condition.validate_param_unsafe(world) } {
799+
ValidationOutcome::Valid => (),
800+
ValidationOutcome::Invalid => {
801+
error_handler(
802+
SystemParamValidationError.into(),
803+
ErrorContext::System {
804+
name: condition.name(),
805+
last_run: condition.get_last_run(),
806+
},
807+
);
808+
return false;
809+
}
810+
ValidationOutcome::Skipped => return false,
802811
}
803812
// SAFETY:
804813
// - The caller ensures that `world` has permission to read any data

crates/bevy_ecs/src/schedule/executor/simple.rs

+28-21
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{
1212
schedule::{
1313
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
1414
},
15-
system::SystemParamValidationError,
15+
system::{SystemParamValidationError, ValidationOutcome},
1616
world::World,
1717
};
1818

@@ -88,17 +88,20 @@ impl SystemExecutor for SimpleExecutor {
8888

8989
let system = &mut schedule.systems[system_index];
9090
if should_run {
91-
let valid_params = system.validate_param(world);
92-
if !valid_params {
93-
error_handler(
94-
SystemParamValidationError.into(),
95-
ErrorContext::System {
96-
name: system.name(),
97-
last_run: system.get_last_run(),
98-
},
99-
);
100-
}
101-
91+
let valid_params = match system.validate_param(world) {
92+
ValidationOutcome::Valid => true,
93+
ValidationOutcome::Invalid => {
94+
error_handler(
95+
SystemParamValidationError.into(),
96+
ErrorContext::System {
97+
name: system.name(),
98+
last_run: system.get_last_run(),
99+
},
100+
);
101+
false
102+
}
103+
ValidationOutcome::Skipped => false,
104+
};
102105
should_run &= valid_params;
103106
}
104107

@@ -173,15 +176,19 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W
173176
conditions
174177
.iter_mut()
175178
.map(|condition| {
176-
if !condition.validate_param(world) {
177-
error_handler(
178-
SystemParamValidationError.into(),
179-
ErrorContext::RunCondition {
180-
name: condition.name(),
181-
last_run: condition.get_last_run(),
182-
},
183-
);
184-
return false;
179+
match condition.validate_param(world) {
180+
ValidationOutcome::Valid => (),
181+
ValidationOutcome::Invalid => {
182+
error_handler(
183+
SystemParamValidationError.into(),
184+
ErrorContext::System {
185+
name: condition.name(),
186+
last_run: condition.get_last_run(),
187+
},
188+
);
189+
return false;
190+
}
191+
ValidationOutcome::Skipped => return false,
185192
}
186193
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
187194
})

0 commit comments

Comments
 (0)