Merge branch 'main' into http-asset-sources

This commit is contained in:
Pete Hayman 2025-05-20 15:29:50 +10:00 committed by GitHub
commit bd5f778a78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 337 additions and 220 deletions

View File

@ -291,9 +291,6 @@ bevy_log = ["bevy_internal/bevy_log"]
# Enable input focus subsystem
bevy_input_focus = ["bevy_internal/bevy_input_focus"]
# Use the configurable global error handler as the default error handler.
configurable_error_handler = ["bevy_internal/configurable_error_handler"]
# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]
@ -2233,7 +2230,6 @@ wasm = false
name = "fallible_params"
path = "examples/ecs/fallible_params.rs"
doc-scrape-examples = true
required-features = ["configurable_error_handler"]
[package.metadata.example.fallible_params]
name = "Fallible System Parameters"
@ -2245,7 +2241,7 @@ wasm = false
name = "error_handling"
path = "examples/ecs/error_handling.rs"
doc-scrape-examples = true
required-features = ["bevy_mesh_picking_backend", "configurable_error_handler"]
required-features = ["bevy_mesh_picking_backend"]
[package.metadata.example.error_handling]
name = "Error handling"

View File

@ -41,7 +41,6 @@ disallowed-methods = [
{ path = "f32::asinh", reason = "use bevy_math::ops::asinh instead for libm determinism" },
{ path = "f32::acosh", reason = "use bevy_math::ops::acosh instead for libm determinism" },
{ path = "f32::atanh", reason = "use bevy_math::ops::atanh instead for libm determinism" },
{ path = "criterion::black_box", reason = "use core::hint::black_box instead" },
]
# Require `bevy_ecs::children!` to use `[]` braces, instead of `()` or `{}`.

View File

@ -10,6 +10,7 @@ use alloc::{
pub use bevy_derive::AppLabel;
use bevy_ecs::{
component::RequiredComponentsError,
error::{DefaultErrorHandler, ErrorHandler},
event::{event_update_system, EventCursor},
intern::Interned,
prelude::*,
@ -85,6 +86,7 @@ pub struct App {
/// [`WinitPlugin`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html
/// [`ScheduleRunnerPlugin`]: https://docs.rs/bevy/latest/bevy/app/struct.ScheduleRunnerPlugin.html
pub(crate) runner: RunnerFn,
default_error_handler: Option<ErrorHandler>,
}
impl Debug for App {
@ -143,6 +145,7 @@ impl App {
sub_apps: HashMap::default(),
},
runner: Box::new(run_once),
default_error_handler: None,
}
}
@ -1115,7 +1118,12 @@ impl App {
}
/// Inserts a [`SubApp`] with the given label.
pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) {
pub fn insert_sub_app(&mut self, label: impl AppLabel, mut sub_app: SubApp) {
if let Some(handler) = self.default_error_handler {
sub_app
.world_mut()
.get_resource_or_insert_with(|| DefaultErrorHandler(handler));
}
self.sub_apps.sub_apps.insert(label.intern(), sub_app);
}
@ -1334,6 +1342,49 @@ impl App {
self.world_mut().add_observer(observer);
self
}
/// Gets the error handler to set for new supapps.
///
/// Note that the error handler of existing subapps may differ.
pub fn get_error_handler(&self) -> Option<ErrorHandler> {
self.default_error_handler
}
/// Set the [default error handler] for the all subapps (including the main one and future ones)
/// that do not have one.
///
/// May only be called once and should be set by the application, not by libraries.
///
/// The handler will be called when an error is produced and not otherwise handled.
///
/// # Panics
/// Panics if called multiple times.
///
/// # Example
/// ```
/// # use bevy_app::*;
/// # use bevy_ecs::error::warn;
/// # fn MyPlugins(_: &mut App) {}
/// App::new()
/// .set_error_handler(warn)
/// .add_plugins(MyPlugins)
/// .run();
/// ```
///
/// [default error handler]: bevy_ecs::error::DefaultErrorHandler
pub fn set_error_handler(&mut self, handler: ErrorHandler) -> &mut Self {
assert!(
self.default_error_handler.is_none(),
"`set_error_handler` called multiple times on same `App`"
);
self.default_error_handler = Some(handler);
for sub_app in self.sub_apps.iter_mut() {
sub_app
.world_mut()
.get_resource_or_insert_with(|| DefaultErrorHandler(handler));
}
self
}
}
type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;

View File

@ -33,13 +33,6 @@ bevy_reflect = ["dep:bevy_reflect"]
## Extends reflection support to functions.
reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]
## Use the configurable global error handler as the default error handler.
##
## This is typically used to turn panics from the ECS into loggable errors.
## This may be useful for production builds,
## but can result in a measurable performance impact, especially for commands.
configurable_error_handler = []
## Enables automatic backtrace capturing in BevyError
backtrace = ["std"]

View File

@ -7,22 +7,17 @@ use crate::{
world::{error::EntityMutableFetchError, World},
};
use super::{default_error_handler, BevyError, ErrorContext};
use super::{BevyError, ErrorContext, ErrorHandler};
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
/// Takes a [`Command`] that potentially returns a Result and uses a given error handler function to convert it into
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
pub trait HandleError<Out = ()> {
pub trait HandleError<Out = ()>: Send + 'static {
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command;
fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command;
/// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
fn handle_error(self) -> impl Command
where
Self: Sized,
{
self.handle_error_with(default_error_handler())
}
fn handle_error(self) -> impl Command;
}
impl<C, T, E> HandleError<Result<T, E>> for C
@ -30,7 +25,7 @@ where
C: Command<Result<T, E>>,
E: Into<BevyError>,
{
fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command {
fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command {
move |world: &mut World| match self.apply(world) {
Ok(_) => {}
Err(err) => (error_handler)(
@ -41,6 +36,18 @@ where
),
}
}
fn handle_error(self) -> impl Command {
move |world: &mut World| match self.apply(world) {
Ok(_) => {}
Err(err) => world.default_error_handler()(
err.into(),
ErrorContext::Command {
name: type_name::<C>().into(),
},
),
}
}
}
impl<C> HandleError<Never> for C
@ -52,6 +59,13 @@ where
self.apply(world);
}
}
#[inline]
fn handle_error(self) -> impl Command {
move |world: &mut World| {
self.apply(world);
}
}
}
impl<C> HandleError for C
@ -63,10 +77,7 @@ where
self
}
#[inline]
fn handle_error(self) -> impl Command
where
Self: Sized,
{
fn handle_error(self) -> impl Command {
self
}
}

View File

@ -1,9 +1,8 @@
#[cfg(feature = "configurable_error_handler")]
use bevy_platform::sync::OnceLock;
use core::fmt::Display;
use crate::{component::Tick, error::BevyError};
use crate::{component::Tick, error::BevyError, prelude::Resource};
use alloc::borrow::Cow;
use derive_more::derive::{Deref, DerefMut};
/// Context for a [`BevyError`] to aid in debugging.
#[derive(Debug, PartialEq, Eq, Clone)]
@ -77,53 +76,6 @@ impl ErrorContext {
}
}
/// A global error handler. This can be set at startup, as long as it is set before
/// any uses. This should generally be configured _before_ initializing the app.
///
/// This should be set inside of your `main` function, before initializing the Bevy app.
/// The value of this error handler can be accessed using the [`default_error_handler`] function,
/// which calls [`OnceLock::get_or_init`] to get the value.
///
/// **Note:** this is only available when the `configurable_error_handler` feature of `bevy_ecs` (or `bevy`) is enabled!
///
/// # Example
///
/// ```
/// # use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, warn};
/// GLOBAL_ERROR_HANDLER.set(warn).expect("The error handler can only be set once, globally.");
/// // initialize Bevy App here
/// ```
///
/// To use this error handler in your app for custom error handling logic:
///
/// ```rust
/// use bevy_ecs::error::{default_error_handler, GLOBAL_ERROR_HANDLER, BevyError, ErrorContext, panic};
///
/// fn handle_errors(error: BevyError, ctx: ErrorContext) {
/// let error_handler = default_error_handler();
/// error_handler(error, ctx);
/// }
/// ```
///
/// # Warning
///
/// As this can *never* be overwritten, library code should never set this value.
#[cfg(feature = "configurable_error_handler")]
pub static GLOBAL_ERROR_HANDLER: OnceLock<fn(BevyError, ErrorContext)> = OnceLock::new();
/// The default error handler. This defaults to [`panic()`],
/// but if set, the [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization.
/// The `configurable_error_handler` feature must be enabled to change this from the panicking default behavior,
/// as there may be runtime overhead.
#[inline]
pub fn default_error_handler() -> fn(BevyError, ErrorContext) {
#[cfg(not(feature = "configurable_error_handler"))]
return panic;
#[cfg(feature = "configurable_error_handler")]
return *GLOBAL_ERROR_HANDLER.get_or_init(|| panic);
}
macro_rules! inner {
($call:path, $e:ident, $c:ident) => {
$call!(
@ -135,6 +87,25 @@ macro_rules! inner {
};
}
/// Defines how Bevy reacts to errors.
pub type ErrorHandler = fn(BevyError, ErrorContext);
/// Error handler to call when an error is not handled otherwise.
/// Defaults to [`panic()`].
///
/// When updated while a [`Schedule`] is running, it doesn't take effect for
/// that schedule until it's completed.
///
/// [`Schedule`]: crate::schedule::Schedule
#[derive(Resource, Deref, DerefMut, Copy, Clone)]
pub struct DefaultErrorHandler(pub ErrorHandler);
impl Default for DefaultErrorHandler {
fn default() -> Self {
Self(panic)
}
}
/// Error handler that panics with the system error.
#[track_caller]
#[inline]

View File

@ -7,8 +7,9 @@
//! All [`BevyError`]s returned by a system, observer or command are handled by an "error handler". By default, the
//! [`panic`] error handler function is used, resulting in a panic with the error message attached.
//!
//! You can change the default behavior by registering a custom error handler.
//! Modify the [`GLOBAL_ERROR_HANDLER`] value to set a custom error handler function for your entire app.
//! You can change the default behavior by registering a custom error handler:
//! Use [`DefaultErrorHandler`] to set a custom error handler function for a world,
//! or `App::set_error_handler` for a whole app.
//! In practice, this is generally feature-flagged: panicking or loudly logging errors in development,
//! and quietly logging or ignoring them in production to avoid crashing the app.
//!
@ -33,10 +34,8 @@
//! The [`ErrorContext`] allows you to access additional details relevant to providing
//! context surrounding the error such as the system's [`name`] in your error messages.
//!
//! Remember to turn on the `configurable_error_handler` feature to set a global error handler!
//!
//! ```rust, ignore
//! use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, BevyError, ErrorContext};
//! use bevy_ecs::error::{BevyError, ErrorContext, DefaultErrorHandler};
//! use log::trace;
//!
//! fn my_error_handler(error: BevyError, ctx: ErrorContext) {
@ -48,10 +47,9 @@
//! }
//!
//! fn main() {
//! // This requires the "configurable_error_handler" feature to be enabled to be in scope.
//! GLOBAL_ERROR_HANDLER.set(my_error_handler).expect("The error handler can only be set once.");
//!
//! // Initialize your Bevy App here
//! let mut world = World::new();
//! world.insert_resource(DefaultErrorHandler(my_error_handler));
//! // Use your world here
//! }
//! ```
//!

View File

@ -3,7 +3,7 @@ use core::any::Any;
use crate::{
component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType},
error::{default_error_handler, ErrorContext},
error::{ErrorContext, ErrorHandler},
observer::{ObserverDescriptor, ObserverTrigger},
prelude::*,
query::DebugCheckedUnwrap,
@ -190,7 +190,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate:
/// [`SystemParam`]: crate::system::SystemParam
pub struct Observer {
hook_on_add: ComponentHook,
error_handler: Option<fn(BevyError, ErrorContext)>,
error_handler: Option<ErrorHandler>,
system: Box<dyn Any + Send + Sync + 'static>,
pub(crate) descriptor: ObserverDescriptor,
pub(crate) last_trigger_id: u32,
@ -232,6 +232,7 @@ impl Observer {
system: Box::new(|| {}),
descriptor: Default::default(),
hook_on_add: |mut world, hook_context| {
let default_error_handler = world.default_error_handler();
world.commands().queue(move |world: &mut World| {
let entity = hook_context.entity;
if let Some(mut observe) = world.get_mut::<Observer>(entity) {
@ -239,7 +240,7 @@ impl Observer {
return;
}
if observe.error_handler.is_none() {
observe.error_handler = Some(default_error_handler());
observe.error_handler = Some(default_error_handler);
}
world.register_observer(entity);
}
@ -348,8 +349,6 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
return;
}
state.last_trigger_id = last_trigger;
// SAFETY: Observer was triggered so must have an `Observer` component.
let error_handler = unsafe { state.error_handler.debug_checked_unwrap() };
let trigger: Trigger<E, B> = Trigger::new(
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
@ -377,7 +376,10 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
match (*system).validate_param_unsafe(world) {
Ok(()) => {
if let Err(err) = (*system).run_unsafe(trigger, world) {
error_handler(
let handler = state
.error_handler
.unwrap_or_else(|| world.default_error_handler());
handler(
err,
ErrorContext::Observer {
name: (*system).name(),
@ -389,7 +391,10 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
}
Err(e) => {
if !e.skipped {
error_handler(
let handler = state
.error_handler
.unwrap_or_else(|| world.default_error_handler());
handler(
e.into(),
ErrorContext::Observer {
name: (*system).name(),
@ -424,9 +429,6 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
observe.descriptor.events.push(event_id);
observe.descriptor.components.extend(components);
if observe.error_handler.is_none() {
observe.error_handler = Some(default_error_handler());
}
let system: *mut dyn ObserverSystem<E, B> = observe.system.downcast_mut::<S>().unwrap();
// SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias
unsafe {
@ -439,7 +441,11 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
#[cfg(test)]
mod tests {
use super::*;
use crate::{event::Event, observer::Trigger};
use crate::{
error::{ignore, DefaultErrorHandler},
event::Event,
observer::Trigger,
};
#[derive(Event)]
struct TriggerEvent;
@ -467,11 +473,20 @@ mod tests {
Err("I failed!".into())
}
// Using observer error handler
let mut world = World::default();
world.init_resource::<Ran>();
let observer = Observer::new(system).with_error_handler(crate::error::ignore);
world.spawn(observer);
Schedule::default().run(&mut world);
world.spawn(Observer::new(system).with_error_handler(ignore));
world.trigger(TriggerEvent);
assert!(world.resource::<Ran>().0);
// Using world error handler
let mut world = World::default();
world.init_resource::<Ran>();
world.spawn(Observer::new(system));
// Test that the correct handler is used when the observer was added
// before the default handler
world.insert_resource(DefaultErrorHandler(ignore));
world.trigger(TriggerEvent);
assert!(world.resource::<Ran>().0);
}

View File

@ -13,7 +13,7 @@ use std::sync::{Mutex, MutexGuard};
use tracing::{info_span, Span};
use crate::{
error::{default_error_handler, BevyError, ErrorContext, Result},
error::{ErrorContext, ErrorHandler, Result},
prelude::Resource,
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
system::ScheduleSystem,
@ -134,7 +134,7 @@ pub struct ExecutorState {
struct Context<'scope, 'env, 'sys> {
environment: &'env Environment<'env, 'sys>,
scope: &'scope Scope<'scope, 'env, ()>,
error_handler: fn(BevyError, ErrorContext),
error_handler: ErrorHandler,
}
impl Default for MultiThreadedExecutor {
@ -240,7 +240,7 @@ impl SystemExecutor for MultiThreadedExecutor {
schedule: &mut SystemSchedule,
world: &mut World,
_skip_systems: Option<&FixedBitSet>,
error_handler: fn(BevyError, ErrorContext),
error_handler: ErrorHandler,
) {
let state = self.state.get_mut().unwrap();
// reset counts
@ -484,6 +484,7 @@ impl ExecutorState {
system,
conditions,
context.environment.world_cell,
context.error_handler,
)
} {
self.skip_system_and_signal_dependents(system_index);
@ -584,9 +585,9 @@ impl ExecutorState {
system: &mut ScheduleSystem,
conditions: &mut Conditions,
world: UnsafeWorldCell,
error_handler: ErrorHandler,
) -> bool {
let mut should_run = !self.skipped_systems.contains(system_index);
let error_handler = default_error_handler();
for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() {
if self.evaluated_sets.contains(set_idx) {
@ -599,7 +600,11 @@ impl ExecutorState {
// required by the conditions.
// - `update_archetype_component_access` has been called for each run condition.
let set_conditions_met = unsafe {
evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world)
evaluate_and_fold_conditions(
&mut conditions.set_conditions[set_idx],
world,
error_handler,
)
};
if !set_conditions_met {
@ -617,7 +622,11 @@ impl ExecutorState {
// required by the conditions.
// - `update_archetype_component_access` has been called for each run condition.
let system_conditions_met = unsafe {
evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world)
evaluate_and_fold_conditions(
&mut conditions.system_conditions[system_index],
world,
error_handler,
)
};
if !system_conditions_met {
@ -822,9 +831,8 @@ fn apply_deferred(
unsafe fn evaluate_and_fold_conditions(
conditions: &mut [BoxedCondition],
world: UnsafeWorldCell,
error_handler: ErrorHandler,
) -> bool {
let error_handler = default_error_handler();
#[expect(
clippy::unnecessary_fold,
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."

View File

@ -10,7 +10,7 @@ use tracing::info_span;
use std::eprintln;
use crate::{
error::{default_error_handler, BevyError, ErrorContext},
error::{ErrorContext, ErrorHandler},
schedule::{
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
},
@ -50,7 +50,7 @@ impl SystemExecutor for SimpleExecutor {
schedule: &mut SystemSchedule,
world: &mut World,
_skip_systems: Option<&FixedBitSet>,
error_handler: fn(BevyError, ErrorContext),
error_handler: ErrorHandler,
) {
// If stepping is enabled, make sure we skip those systems that should
// not be run.
@ -73,8 +73,11 @@ impl SystemExecutor for SimpleExecutor {
}
// evaluate system set's conditions
let set_conditions_met =
evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world);
let set_conditions_met = evaluate_and_fold_conditions(
&mut schedule.set_conditions[set_idx],
world,
error_handler,
);
if !set_conditions_met {
self.completed_systems
@ -86,8 +89,11 @@ impl SystemExecutor for SimpleExecutor {
}
// evaluate system's conditions
let system_conditions_met =
evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world);
let system_conditions_met = evaluate_and_fold_conditions(
&mut schedule.system_conditions[system_index],
world,
error_handler,
);
should_run &= system_conditions_met;
@ -175,9 +181,11 @@ impl SimpleExecutor {
since = "0.17.0",
note = "Use SingleThreadedExecutor instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation."
)]
fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool {
let error_handler = default_error_handler();
fn evaluate_and_fold_conditions(
conditions: &mut [BoxedCondition],
world: &mut World,
error_handler: ErrorHandler,
) -> bool {
#[expect(
clippy::unnecessary_fold,
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."

View File

@ -8,7 +8,7 @@ use tracing::info_span;
use std::eprintln;
use crate::{
error::{default_error_handler, BevyError, ErrorContext},
error::{ErrorContext, ErrorHandler},
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
world::World,
};
@ -50,7 +50,7 @@ impl SystemExecutor for SingleThreadedExecutor {
schedule: &mut SystemSchedule,
world: &mut World,
_skip_systems: Option<&FixedBitSet>,
error_handler: fn(BevyError, ErrorContext),
error_handler: ErrorHandler,
) {
// If stepping is enabled, make sure we skip those systems that should
// not be run.
@ -73,8 +73,11 @@ impl SystemExecutor for SingleThreadedExecutor {
}
// evaluate system set's conditions
let set_conditions_met =
evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world);
let set_conditions_met = evaluate_and_fold_conditions(
&mut schedule.set_conditions[set_idx],
world,
error_handler,
);
if !set_conditions_met {
self.completed_systems
@ -86,8 +89,11 @@ impl SystemExecutor for SingleThreadedExecutor {
}
// evaluate system's conditions
let system_conditions_met =
evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world);
let system_conditions_met = evaluate_and_fold_conditions(
&mut schedule.system_conditions[system_index],
world,
error_handler,
);
should_run &= system_conditions_met;
@ -193,9 +199,11 @@ impl SingleThreadedExecutor {
}
}
fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool {
let error_handler: fn(BevyError, ErrorContext) = default_error_handler();
fn evaluate_and_fold_conditions(
conditions: &mut [BoxedCondition],
world: &mut World,
error_handler: ErrorHandler,
) -> bool {
#[expect(
clippy::unnecessary_fold,
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."

View File

@ -27,7 +27,6 @@ use tracing::info_span;
use crate::{
component::{ComponentId, Components, Tick},
error::default_error_handler,
prelude::Component,
resource::Resource,
schedule::*,
@ -442,7 +441,7 @@ impl Schedule {
self.initialize(world)
.unwrap_or_else(|e| panic!("Error when initializing schedule {:?}: {e}", self.label));
let error_handler = default_error_handler();
let error_handler = world.default_error_handler();
#[cfg(not(feature = "bevy_debug_stepping"))]
self.executor
@ -2061,6 +2060,7 @@ mod tests {
use bevy_ecs_macros::ScheduleLabel;
use crate::{
error::{ignore, panic, DefaultErrorHandler, Result},
prelude::{ApplyDeferred, Res, Resource},
schedule::{
tests::ResMut, IntoScheduleConfigs, Schedule, ScheduleBuildSettings, SystemSet,
@ -2810,4 +2810,32 @@ mod tests {
.expect("CheckSystemRan Resource Should Exist");
assert_eq!(value.0, 2);
}
#[test]
fn test_default_error_handler() {
#[derive(Resource, Default)]
struct Ran(bool);
fn system(mut ran: ResMut<Ran>) -> Result {
ran.0 = true;
Err("I failed!".into())
}
// Test that the default error handler is used
let mut world = World::default();
world.init_resource::<Ran>();
world.insert_resource(DefaultErrorHandler(ignore));
let mut schedule = Schedule::default();
schedule.add_systems(system).run(&mut world);
assert!(world.resource::<Ran>().0);
// Test that the handler doesn't change within the schedule
schedule.add_systems(
(|world: &mut World| {
world.insert_resource(DefaultErrorHandler(panic));
})
.before(system),
);
schedule.run(&mut world);
}
}

View File

@ -88,8 +88,8 @@ use crate::{
/// A [`Command`] can return a [`Result`](crate::error::Result),
/// which will be passed to an [error handler](crate::error) if the `Result` is an error.
///
/// The [default error handler](crate::error::default_error_handler) panics.
/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`.
/// The default error handler panics. It can be configured via
/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource.
///
/// Alternatively, you can customize the error handler for a specific command
/// by calling [`Commands::queue_handled`].
@ -508,7 +508,7 @@ impl<'w, 's> Commands<'w, 's> {
/// Pushes a generic [`Command`] to the command queue.
///
/// If the [`Command`] returns a [`Result`],
/// it will be handled using the [default error handler](crate::error::default_error_handler).
/// it will be handled using the [default error handler](crate::error::DefaultErrorHandler).
///
/// To use a custom error handler, see [`Commands::queue_handled`].
///
@ -643,7 +643,7 @@ impl<'w, 's> Commands<'w, 's> {
/// This command will fail if any of the given entities do not exist.
///
/// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError),
/// which will be handled by the [default error handler](crate::error::default_error_handler).
/// which will be handled by the [default error handler](crate::error::DefaultErrorHandler).
#[track_caller]
pub fn insert_batch<I, B>(&mut self, batch: I)
where
@ -674,7 +674,7 @@ impl<'w, 's> Commands<'w, 's> {
/// This command will fail if any of the given entities do not exist.
///
/// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError),
/// which will be handled by the [default error handler](crate::error::default_error_handler).
/// which will be handled by the [default error handler](crate::error::DefaultErrorHandler).
#[track_caller]
pub fn insert_batch_if_new<I, B>(&mut self, batch: I)
where
@ -1175,8 +1175,8 @@ impl<'w, 's> Commands<'w, 's> {
/// An [`EntityCommand`] can return a [`Result`](crate::error::Result),
/// which will be passed to an [error handler](crate::error) if the `Result` is an error.
///
/// The [default error handler](crate::error::default_error_handler) panics.
/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`.
/// The default error handler panics. It can be configured via
/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource.
///
/// Alternatively, you can customize the error handler for a specific command
/// by calling [`EntityCommands::queue_handled`].
@ -1768,7 +1768,7 @@ impl<'a> EntityCommands<'a> {
/// Pushes an [`EntityCommand`] to the queue,
/// which will get executed for the current [`Entity`].
///
/// The [default error handler](crate::error::default_error_handler)
/// The [default error handler](crate::error::DefaultErrorHandler)
/// will be used to handle error cases.
/// Every [`EntityCommand`] checks whether the entity exists at the time of execution
/// and returns an error if it does not.

View File

@ -10,7 +10,7 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship
/// Infinite loops are possible, and are not checked for. While looping can be desirable in some contexts
/// (for example, an observer that triggers itself multiple times before stopping), following an infinite
/// traversal loop without an eventual exit will cause your application to hang. Each implementer of `Traversal`
/// for documenting possible looping behavior, and consumers of those implementations are responsible for
/// is responsible for documenting possible looping behavior, and consumers of those implementations are responsible for
/// avoiding infinite loops in their code.
///
/// Traversals may be parameterized with additional data. For example, in observer event propagation, the

View File

@ -14,6 +14,7 @@ pub mod unsafe_world_cell;
#[cfg(feature = "bevy_reflect")]
pub mod reflect;
use crate::error::{DefaultErrorHandler, ErrorHandler};
pub use crate::{
change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD},
world::command_queue::CommandQueue,
@ -3046,6 +3047,16 @@ impl World {
// SAFETY: We just initialized the bundle so its id should definitely be valid.
unsafe { self.bundles.get(id).debug_checked_unwrap() }
}
/// Convenience method for accessing the world's default error handler,
/// which can be overwritten with [`DefaultErrorHandler`].
#[inline]
pub fn default_error_handler(&self) -> ErrorHandler {
self.get_resource::<DefaultErrorHandler>()
.copied()
.unwrap_or_default()
.0
}
}
impl World {

View File

@ -7,6 +7,7 @@ use crate::{
change_detection::{MaybeLocation, MutUntyped, Ticks, TicksMut},
component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells},
entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation},
error::{DefaultErrorHandler, ErrorHandler},
observer::Observers,
prelude::Component,
query::{DebugCheckedUnwrap, ReadOnlyQueryData},
@ -705,6 +706,18 @@ impl<'w> UnsafeWorldCell<'w> {
(*self.ptr).last_trigger_id = (*self.ptr).last_trigger_id.wrapping_add(1);
}
}
/// Convenience method for accessing the world's default error handler,
///
/// # Safety
/// Must have read access to [`DefaultErrorHandler`].
#[inline]
pub unsafe fn default_error_handler(&self) -> ErrorHandler {
self.get_resource::<DefaultErrorHandler>()
.copied()
.unwrap_or_default()
.0
}
}
impl Debug for UnsafeWorldCell<'_> {

View File

@ -285,9 +285,6 @@ custom_cursor = ["bevy_winit/custom_cursor"]
# Experimental support for nodes that are ignored for UI layouting
ghost_nodes = ["bevy_ui/ghost_nodes"]
# Use the configurable global error handler as the default error handler.
configurable_error_handler = ["bevy_ecs/configurable_error_handler"]
# Allows access to the `std` crate. Enabling this feature will prevent compilation
# on `no_std` targets, but provides access to certain additional features on
# supported platforms.

View File

@ -95,7 +95,7 @@ impl BevyManifest {
return None;
};
let mut path = Self::parse_str::<syn::Path>(&format!("::{}", package));
let mut path = Self::parse_str::<syn::Path>(&format!("::{package}"));
if let Some(module) = name.strip_prefix("bevy_") {
path.segments.push(Self::parse_str(module));
}

View File

@ -983,12 +983,18 @@ pub fn specialize_prepass_material_meshes<M>(
AlphaMode::Blend
| AlphaMode::Premultiplied
| AlphaMode::Add
| AlphaMode::Multiply => continue,
| AlphaMode::Multiply => {
// In case this material was previously in a valid alpha_mode, remove it to
// stop the queue system from assuming its retained cache to be valid.
view_specialized_material_pipeline_cache.remove(visible_entity);
continue;
}
}
if material.properties.reads_view_transmission_texture {
// No-op: Materials reading from `ViewTransmissionTexture` are not rendered in the `Opaque3d`
// phase, and are therefore also excluded from the prepass much like alpha-blended materials.
view_specialized_material_pipeline_cache.remove(visible_entity);
continue;
}

View File

@ -220,7 +220,18 @@ pub fn extract_lights(
mut commands: Commands,
point_light_shadow_map: Extract<Res<PointLightShadowMap>>,
directional_light_shadow_map: Extract<Res<DirectionalLightShadowMap>>,
global_point_lights: Extract<Res<GlobalVisibleClusterableObjects>>,
global_visible_clusterable: Extract<Res<GlobalVisibleClusterableObjects>>,
previous_point_lights: Query<
Entity,
(
With<RenderCubemapVisibleEntities>,
With<ExtractedPointLight>,
),
>,
previous_spot_lights: Query<
Entity,
(With<RenderVisibleMeshEntities>, With<ExtractedPointLight>),
>,
point_lights: Extract<
Query<(
Entity,
@ -276,6 +287,22 @@ pub fn extract_lights(
if directional_light_shadow_map.is_changed() {
commands.insert_resource(directional_light_shadow_map.clone());
}
// Clear previous visible entities for all point/spot lights as they might not be in the
// `global_visible_clusterable` list anymore.
commands.try_insert_batch(
previous_point_lights
.iter()
.map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default()))
.collect::<Vec<_>>(),
);
commands.try_insert_batch(
previous_spot_lights
.iter()
.map(|render_entity| (render_entity, RenderVisibleMeshEntities::default()))
.collect::<Vec<_>>(),
);
// This is the point light shadow map texel size for one face of the cube as a distance of 1.0
// world unit from the light.
// point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels
@ -286,7 +313,7 @@ pub fn extract_lights(
let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32;
let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len);
for entity in global_point_lights.iter().copied() {
for entity in global_visible_clusterable.iter().copied() {
let Ok((
main_entity,
render_entity,
@ -350,7 +377,7 @@ pub fn extract_lights(
commands.try_insert_batch(point_lights_values);
let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len);
for entity in global_point_lights.iter().copied() {
for entity in global_visible_clusterable.iter().copied() {
if let Ok((
main_entity,
render_entity,

View File

@ -84,7 +84,7 @@ pub struct PointerHits {
}
impl PointerHits {
#[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
/// Construct [`PointerHits`].
pub fn new(pointer: prelude::PointerId, picks: Vec<(Entity, HitData)>, order: f32) -> Self {
Self {
pointer,
@ -114,7 +114,7 @@ pub struct HitData {
}
impl HitData {
#[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
/// Construct a [`HitData`].
pub fn new(camera: Entity, depth: f32, position: Option<Vec3>, normal: Option<Vec3>) -> Self {
Self {
camera,

View File

@ -203,27 +203,24 @@ impl ShaderCache {
shaders: &HashMap<AssetId<Shader>, Shader>,
import: &ShaderImport,
) -> Result<(), PipelineCacheError> {
if !composer.contains_module(&import.module_name()) {
if let Some(shader_handle) = import_path_shaders.get(import) {
if let Some(shader) = shaders.get(shader_handle) {
// Early out if we've already imported this module
if composer.contains_module(&import.module_name()) {
return Ok(());
}
// Check if the import is available (this handles the recursive import case)
let shader = import_path_shaders
.get(import)
.and_then(|handle| shaders.get(handle))
.ok_or(PipelineCacheError::ShaderImportNotYetAvailable)?;
// Recurse down to ensure all import dependencies are met
for import in &shader.imports {
Self::add_import_to_composer(
composer,
import_path_shaders,
shaders,
import,
)?;
Self::add_import_to_composer(composer, import_path_shaders, shaders, import)?;
}
composer.add_composable_module(shader.into())?;
} else {
Err(PipelineCacheError::ShaderImportNotYetAvailable)?;
}
} else {
Err(PipelineCacheError::ShaderImportNotYetAvailable)?;
}
// if we fail to add a module the composer will tell us what is missing
}
Ok(())
}

View File

@ -87,18 +87,6 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf");
#[derive(Default)]
pub struct TextPlugin;
/// Text is rendered for two different view projections;
/// 2-dimensional text ([`Text2d`]) is rendered in "world space" with a `BottomToTop` Y-axis,
/// while UI is rendered with a `TopToBottom` Y-axis.
/// This matters for text because the glyph positioning is different in either layout.
/// For `TopToBottom`, 0 is the top of the text, while for `BottomToTop` 0 is the bottom.
pub enum YAxisOrientation {
/// Top to bottom Y-axis orientation, for UI
TopToBottom,
/// Bottom to top Y-axis orientation, for 2d world space
BottomToTop,
}
/// System set in [`PostUpdate`] where all 2d text update systems are executed.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub struct Text2dUpdateSystems;

View File

@ -17,7 +17,7 @@ use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};
use crate::{
error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText,
LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, YAxisOrientation,
LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout,
};
/// A wrapper resource around a [`cosmic_text::FontSystem`]
@ -228,7 +228,6 @@ impl TextPipeline {
font_atlas_sets: &mut FontAtlasSets,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
y_axis_orientation: YAxisOrientation,
computed: &mut ComputedTextBlock,
font_system: &mut CosmicFontSystem,
swash_cache: &mut SwashCache,
@ -348,10 +347,6 @@ impl TextPipeline {
let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32;
let y =
line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0;
let y = match y_axis_orientation {
YAxisOrientation::TopToBottom => y,
YAxisOrientation::BottomToTop => box_size.y - y,
};
let position = Vec2::new(x, y);

View File

@ -2,7 +2,7 @@ use crate::pipeline::CosmicFontSystem;
use crate::{
ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBounds,
TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot,
TextSpanAccess, TextWriter, YAxisOrientation,
TextSpanAccess, TextWriter,
};
use bevy_asset::Assets;
use bevy_color::LinearRgba;
@ -182,10 +182,10 @@ pub fn extract_text2d_sprite(
text_bounds.width.unwrap_or(text_layout_info.size.x),
text_bounds.height.unwrap_or(text_layout_info.size.y),
);
let bottom_left =
-(anchor.as_vec() + 0.5) * size + (size.y - text_layout_info.size.y) * Vec2::Y;
let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size;
let transform =
*global_transform * GlobalTransform::from_translation(bottom_left.extend(0.)) * scaling;
*global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling;
let mut color = LinearRgba::WHITE;
let mut current_span = usize::MAX;
@ -218,7 +218,7 @@ pub fn extract_text2d_sprite(
.textures[atlas_info.location.glyph_index]
.as_rect();
extracted_slices.slices.push(ExtractedSlice {
offset: *position,
offset: Vec2::new(position.x, -position.y),
rect,
size: rect.size(),
});
@ -316,7 +316,6 @@ pub fn update_text2d_layout(
&mut font_atlas_sets,
&mut texture_atlases,
&mut textures,
YAxisOrientation::BottomToTop,
computed.as_mut(),
&mut font_system,
&mut swash_cache,

View File

@ -20,7 +20,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_text::{
scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache,
TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo,
TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, YAxisOrientation,
TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter,
};
use taffy::style::AvailableSpace;
use tracing::error;
@ -328,7 +328,6 @@ fn queue_text(
font_atlas_sets,
texture_atlases,
textures,
YAxisOrientation::TopToBottom,
computed,
font_system,
swash_cache,

View File

@ -70,7 +70,6 @@ The default feature set enables most of the expected features of a game engine,
|bevy_remote|Enable the Bevy Remote Protocol|
|bevy_ui_debug|Provides a debug overlay for bevy UI|
|bmp|BMP image format support|
|configurable_error_handler|Use the configurable global error handler as the default error handler.|
|critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.|
|dds|DDS compressed texture support|
|debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam|

View File

@ -1,13 +1,7 @@
//! Showcases how fallible systems and observers can make use of Rust's powerful result handling
//! syntax.
//!
//! Important note: to set the global error handler, the `configurable_error_handler` feature must be
//! enabled. This feature is disabled by default, as it may introduce runtime overhead, especially for commands.
use bevy::ecs::{
error::{warn, GLOBAL_ERROR_HANDLER},
world::DeferredWorld,
};
use bevy::ecs::{error::warn, world::DeferredWorld};
use bevy::math::sampling::UniformMeshSampler;
use bevy::prelude::*;
@ -16,17 +10,15 @@ use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
fn main() {
let mut app = App::new();
// By default, fallible systems that return an error will panic.
//
// We can change this by setting a custom error handler, which applies globally.
// Here we set the global error handler using one of the built-in
// error handlers. Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`,
// We can change this by setting a custom error handler, which applies to the entire app
// (you can also set it for specific `World`s).
// Here we it using one of the built-in error handlers.
// Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`,
// `debug`, `trace` and `ignore`.
GLOBAL_ERROR_HANDLER
.set(warn)
.expect("The error handler can only be set once, globally.");
let mut app = App::new();
app.set_error_handler(warn);
app.add_plugins(DefaultPlugins);

View File

@ -2,7 +2,7 @@
//! from running if their acquiry conditions aren't met.
//!
//! Fallible system parameters include:
//! - [`Res<R>`], [`ResMut<R>`] - Resource has to exist, and the [`GLOBAL_ERROR_HANDLER`] will be called if it doesn't.
//! - [`Res<R>`], [`ResMut<R>`] - Resource has to exist, and the [`World::get_default_error_handler`] will be called if it doesn't.
//! - [`Single<D, F>`] - There must be exactly one matching entity, but the system will be silently skipped otherwise.
//! - [`Option<Single<D, F>>`] - There must be zero or one matching entity. The system will be silently skipped if there are more.
//! - [`Populated<D, F>`] - There must be at least one matching entity, but the system will be silently skipped otherwise.
@ -18,19 +18,13 @@
//!
//! [`SystemParamValidationError`]: bevy::ecs::system::SystemParamValidationError
//! [`SystemParam::validate_param`]: bevy::ecs::system::SystemParam::validate_param
//! [`default_error_handler`]: bevy::ecs::error::default_error_handler
use bevy::ecs::error::{warn, GLOBAL_ERROR_HANDLER};
use bevy::ecs::error::warn;
use bevy::prelude::*;
use rand::Rng;
fn main() {
// By default, if a parameter fail to be fetched,
// the `GLOBAL_ERROR_HANDLER` will be used to handle the error,
// which by default is set to panic.
GLOBAL_ERROR_HANDLER
.set(warn)
.expect("The error handler can only be set once, globally.");
println!();
println!("Press 'A' to add enemy ships and 'R' to remove them.");
println!("Player ship will wait for enemy ships and track one if it exists,");
@ -38,6 +32,10 @@ fn main() {
println!();
App::new()
// By default, if a parameter fail to be fetched,
// `World::get_default_error_handler` will be used to handle the error,
// which by default is set to panic.
.set_error_handler(warn)
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (user_input, move_targets, track_targets).chain())

View File

@ -0,0 +1,10 @@
---
title: Global default error handler
pull_requests: [18810]
---
Worlds can now have different default error handlers, so there no longer is a global handler.
Replace uses of `GLOBAL_ERROR_HANDLER` with `App`'s `.set_error_handler(handler)`.
For worlds that do not directly belong to an `App`/`SubApp`,
insert the `DefaultErrorHandler(handler)` resource.