Merge branch 'main' into nuke_get_base_descriptor

This commit is contained in:
Emerson Coskey 2025-07-17 10:08:28 -07:00
commit 88b3b5e71e
No known key found for this signature in database
21 changed files with 652 additions and 333 deletions

View File

@ -3068,6 +3068,17 @@ description = "Test rendering of many UI elements"
category = "Stress Tests" category = "Stress Tests"
wasm = true wasm = true
[[example]]
name = "many_gradients"
path = "examples/stress_tests/many_gradients.rs"
doc-scrape-examples = true
[package.metadata.example.many_gradients]
name = "Many Gradients"
description = "Stress test for gradient rendering performance"
category = "Stress Tests"
wasm = true
[[example]] [[example]]
name = "many_cameras_lights" name = "many_cameras_lights"
path = "examples/stress_tests/many_cameras_lights.rs" path = "examples/stress_tests/many_cameras_lights.rs"

View File

@ -12,7 +12,7 @@ struct RegisteredEvent {
event_key: EventKey, event_key: EventKey,
// Required to flush the secondary buffer and drop events even if left unchanged. // Required to flush the secondary buffer and drop events even if left unchanged.
previously_updated: bool, previously_updated: bool,
// SAFETY: The component ID and the function must be used to fetch the Events<T> resource // SAFETY: The `EventKey`'s component ID and the function must be used to fetch the Events<T> resource
// of the same type initialized in `register_event`, or improper type casts will occur. // of the same type initialized in `register_event`, or improper type casts will occur.
update: unsafe fn(MutUntyped), update: unsafe fn(MutUntyped),
} }

View File

@ -31,20 +31,16 @@ pub fn signal_event_update_system(signal: Option<ResMut<EventRegistry>>) {
/// A system that calls [`Events::update`](super::Events::update) on all registered [`Events`][super::Events] in the world. /// A system that calls [`Events::update`](super::Events::update) on all registered [`Events`][super::Events] in the world.
pub fn event_update_system(world: &mut World, mut last_change_tick: Local<Tick>) { pub fn event_update_system(world: &mut World, mut last_change_tick: Local<Tick>) {
if world.contains_resource::<EventRegistry>() { world.try_resource_scope(|world, mut registry: Mut<EventRegistry>| {
world.resource_scope(|world, mut registry: Mut<EventRegistry>| {
registry.run_updates(world, *last_change_tick); registry.run_updates(world, *last_change_tick);
registry.should_update = match registry.should_update { registry.should_update = match registry.should_update {
// If we're always updating, keep doing so. // If we're always updating, keep doing so.
ShouldUpdateEvents::Always => ShouldUpdateEvents::Always, ShouldUpdateEvents::Always => ShouldUpdateEvents::Always,
// Disable the system until signal_event_update_system runs again. // Disable the system until signal_event_update_system runs again.
ShouldUpdateEvents::Waiting | ShouldUpdateEvents::Ready => { ShouldUpdateEvents::Waiting | ShouldUpdateEvents::Ready => ShouldUpdateEvents::Waiting,
ShouldUpdateEvents::Waiting
}
}; };
}); });
}
*last_change_tick = world.change_tick(); *last_change_tick = world.change_tick();
} }

View File

@ -248,7 +248,7 @@ impl Observer {
world.commands().queue(move |world: &mut World| { world.commands().queue(move |world: &mut World| {
let entity = hook_context.entity; let entity = hook_context.entity;
if let Some(mut observe) = world.get_mut::<Observer>(entity) { if let Some(mut observe) = world.get_mut::<Observer>(entity) {
if observe.descriptor.events.is_empty() { if observe.descriptor.event_keys.is_empty() {
return; return;
} }
if observe.error_handler.is_none() { if observe.error_handler.is_none() {
@ -286,13 +286,13 @@ impl Observer {
self self
} }
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] /// Observe the given `event_key`. This will cause the [`Observer`] to run whenever an event with the given [`EventKey`]
/// is triggered. /// is triggered.
/// # Safety /// # Safety
/// The type of the `event` [`EventKey`] _must_ match the actual value /// The type of the `event_key` [`EventKey`] _must_ match the actual value
/// of the event passed into the observer system. /// of the event passed into the observer system.
pub unsafe fn with_event(mut self, event: EventKey) -> Self { pub unsafe fn with_event_key(mut self, event_key: EventKey) -> Self {
self.descriptor.events.push(event); self.descriptor.event_keys.push(event_key);
self self
} }
@ -349,8 +349,8 @@ impl Component for Observer {
/// This information is stored inside of the [`Observer`] component, /// This information is stored inside of the [`Observer`] component,
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct ObserverDescriptor { pub struct ObserverDescriptor {
/// The events the observer is watching. /// The event keys the observer is watching.
pub(super) events: Vec<EventKey>, pub(super) event_keys: Vec<EventKey>,
/// The components the observer is watching. /// The components the observer is watching.
pub(super) components: Vec<ComponentId>, pub(super) components: Vec<ComponentId>,
@ -360,12 +360,12 @@ pub struct ObserverDescriptor {
} }
impl ObserverDescriptor { impl ObserverDescriptor {
/// Add the given `events` to the descriptor. /// Add the given `event_keys` to the descriptor.
/// # Safety /// # Safety
/// The type of each [`EventKey`] in `events` _must_ match the actual value /// The type of each [`EventKey`] in `event_keys` _must_ match the actual value
/// of the event passed into the observer. /// of the event passed into the observer.
pub unsafe fn with_events(mut self, events: Vec<EventKey>) -> Self { pub unsafe fn with_event_keys(mut self, event_keys: Vec<EventKey>) -> Self {
self.events = events; self.event_keys = event_keys;
self self
} }
@ -381,9 +381,9 @@ impl ObserverDescriptor {
self self
} }
/// Returns the `events` that the observer is watching. /// Returns the `event_keys` that the observer is watching.
pub fn events(&self) -> &[EventKey] { pub fn event_keys(&self) -> &[EventKey] {
&self.events &self.event_keys
} }
/// Returns the `components` that the observer is watching. /// Returns the `components` that the observer is watching.
@ -416,7 +416,7 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
components.push(id); components.push(id);
}); });
if let Some(mut observer) = world.get_mut::<Observer>(entity) { if let Some(mut observer) = world.get_mut::<Observer>(entity) {
observer.descriptor.events.push(event_key); observer.descriptor.event_keys.push(event_key);
observer.descriptor.components.extend(components); observer.descriptor.components.extend(components);
let system: &mut dyn Any = observer.system.as_mut(); let system: &mut dyn Any = observer.system.as_mut();

View File

@ -43,7 +43,7 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo
.get_mut::<Observer>(observer_entity) .get_mut::<Observer>(observer_entity)
.expect("Source observer entity must have Observer"); .expect("Source observer entity must have Observer");
observer_state.descriptor.entities.push(target); observer_state.descriptor.entities.push(target);
let event_keys = observer_state.descriptor.events.clone(); let event_keys = observer_state.descriptor.event_keys.clone();
let components = observer_state.descriptor.components.clone(); let components = observer_state.descriptor.components.clone();
for event_key in event_keys { for event_key in event_keys {
let observers = world.observers.get_observers_mut(event_key); let observers = world.observers.get_observers_mut(event_key);

View File

@ -379,7 +379,7 @@ impl World {
}; };
let descriptor = &observer_state.descriptor; let descriptor = &observer_state.descriptor;
for &event_key in &descriptor.events { for &event_key in &descriptor.event_keys {
let cache = observers.get_observers_mut(event_key); let cache = observers.get_observers_mut(event_key);
if descriptor.components.is_empty() && descriptor.entities.is_empty() { if descriptor.components.is_empty() && descriptor.entities.is_empty() {
@ -430,7 +430,7 @@ impl World {
let archetypes = &mut self.archetypes; let archetypes = &mut self.archetypes;
let observers = &mut self.observers; let observers = &mut self.observers;
for &event_key in &descriptor.events { for &event_key in &descriptor.event_keys {
let cache = observers.get_observers_mut(event_key); let cache = observers.get_observers_mut(event_key);
if descriptor.components.is_empty() && descriptor.entities.is_empty() { if descriptor.components.is_empty() && descriptor.entities.is_empty() {
cache.global_observers.remove(&entity); cache.global_observers.remove(&entity);
@ -741,7 +741,7 @@ mod tests {
Observer::new(|_: On<Add, A>, mut res: ResMut<Order>| { Observer::new(|_: On<Add, A>, mut res: ResMut<Order>| {
res.observed("add/remove"); res.observed("add/remove");
}) })
.with_event(on_remove) .with_event_key(on_remove)
}, },
); );
@ -1015,7 +1015,7 @@ mod tests {
Observer::with_dynamic_runner(|mut world, _trigger, _ptr, _propagate| { Observer::with_dynamic_runner(|mut world, _trigger, _ptr, _propagate| {
world.resource_mut::<Order>().observed("event_a"); world.resource_mut::<Order>().observed("event_a");
}) })
.with_event(event_a) .with_event_key(event_a)
}; };
world.spawn(observe); world.spawn(observe);

View File

@ -181,7 +181,7 @@ impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> {
pub struct ObserverTrigger { pub struct ObserverTrigger {
/// The [`Entity`] of the observer handling the trigger. /// The [`Entity`] of the observer handling the trigger.
pub observer: Entity, pub observer: Entity,
/// The [`Event`] the trigger targeted. /// The [`EventKey`] the trigger targeted.
pub event_key: EventKey, pub event_key: EventKey,
/// The [`ComponentId`]s the trigger targeted. /// The [`ComponentId`]s the trigger targeted.
pub components: SmallVec<[ComponentId; 2]>, pub components: SmallVec<[ComponentId; 2]>,

View File

@ -1,10 +1,9 @@
use crate::{ use crate::{
entity::Entity,
prelude::Mut, prelude::Mut,
reflect::{AppTypeRegistry, ReflectBundle, ReflectComponent}, reflect::{AppTypeRegistry, ReflectBundle, ReflectComponent},
resource::Resource, resource::Resource,
system::EntityCommands, system::EntityCommands,
world::{EntityWorldMut, World}, world::EntityWorldMut,
}; };
use alloc::{borrow::Cow, boxed::Box}; use alloc::{borrow::Cow, boxed::Box};
use bevy_reflect::{PartialReflect, TypeRegistry}; use bevy_reflect::{PartialReflect, TypeRegistry};
@ -22,7 +21,7 @@ pub trait ReflectCommandExt {
/// - If [`AppTypeRegistry`] does not have the reflection data for the given /// - If [`AppTypeRegistry`] does not have the reflection data for the given
/// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle). /// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle).
/// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details. /// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details.
/// - If [`AppTypeRegistry`] is not present in the [`World`]. /// - If [`AppTypeRegistry`] is not present in the [`World`](crate::world::World).
/// ///
/// # Note /// # Note
/// ///
@ -92,11 +91,11 @@ pub trait ReflectCommandExt {
/// ///
/// # Panics /// # Panics
/// ///
/// - If the given [`Resource`] is not present in the [`World`]. /// - If the given [`Resource`] is not present in the [`World`](crate::world::World).
/// ///
/// # Note /// # Note
/// ///
/// - The given [`Resource`] is removed from the [`World`] before the command is applied. /// - The given [`Resource`] is removed from the [`World`](crate::world::World) before the command is applied.
fn insert_reflect_with_registry<T: Resource + AsRef<TypeRegistry>>( fn insert_reflect_with_registry<T: Resource + AsRef<TypeRegistry>>(
&mut self, &mut self,
component: Box<dyn PartialReflect>, component: Box<dyn PartialReflect>,
@ -214,7 +213,7 @@ impl<'w> EntityWorldMut<'w> {
/// - If [`AppTypeRegistry`] does not have the reflection data for the given /// - If [`AppTypeRegistry`] does not have the reflection data for the given
/// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle). /// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle).
/// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details. /// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details.
/// - If [`AppTypeRegistry`] is not present in the [`World`]. /// - If [`AppTypeRegistry`] is not present in the [`World`](crate::world::World).
/// ///
/// # Note /// # Note
/// ///
@ -222,15 +221,10 @@ impl<'w> EntityWorldMut<'w> {
/// is much slower. /// is much slower.
pub fn insert_reflect(&mut self, component: Box<dyn PartialReflect>) -> &mut Self { pub fn insert_reflect(&mut self, component: Box<dyn PartialReflect>) -> &mut Self {
self.assert_not_despawned(); self.assert_not_despawned();
let entity_id = self.id(); self.resource_scope(|entity, registry: Mut<AppTypeRegistry>| {
self.world_scope(|world| {
world.resource_scope(|world, registry: Mut<AppTypeRegistry>| {
let type_registry = &registry.as_ref().read(); let type_registry = &registry.as_ref().read();
insert_reflect_with_registry_ref(world, entity_id, type_registry, component); insert_reflect_with_registry_ref(entity, type_registry, component);
}); });
world.flush();
});
self.update_location();
self self
} }
@ -245,21 +239,16 @@ impl<'w> EntityWorldMut<'w> {
/// - If the given [`Resource`] does not have the reflection data for the given /// - If the given [`Resource`] does not have the reflection data for the given
/// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle). /// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle).
/// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details. /// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details.
/// - If the given [`Resource`] is not present in the [`World`]. /// - If the given [`Resource`] is not present in the [`World`](crate::world::World).
pub fn insert_reflect_with_registry<T: Resource + AsRef<TypeRegistry>>( pub fn insert_reflect_with_registry<T: Resource + AsRef<TypeRegistry>>(
&mut self, &mut self,
component: Box<dyn PartialReflect>, component: Box<dyn PartialReflect>,
) -> &mut Self { ) -> &mut Self {
self.assert_not_despawned(); self.assert_not_despawned();
let entity_id = self.id(); self.resource_scope(|entity, registry: Mut<T>| {
self.world_scope(|world| {
world.resource_scope(|world, registry: Mut<T>| {
let type_registry = registry.as_ref().as_ref(); let type_registry = registry.as_ref().as_ref();
insert_reflect_with_registry_ref(world, entity_id, type_registry, component); insert_reflect_with_registry_ref(entity, type_registry, component);
}); });
world.flush();
});
self.update_location();
self self
} }
@ -275,7 +264,7 @@ impl<'w> EntityWorldMut<'w> {
/// # Panics /// # Panics
/// ///
/// - If the entity has been despawned while this `EntityWorldMut` is still alive. /// - If the entity has been despawned while this `EntityWorldMut` is still alive.
/// - If [`AppTypeRegistry`] is not present in the [`World`]. /// - If [`AppTypeRegistry`] is not present in the [`World`](crate::world::World).
/// ///
/// # Note /// # Note
/// ///
@ -283,20 +272,10 @@ impl<'w> EntityWorldMut<'w> {
/// is much slower. /// is much slower.
pub fn remove_reflect(&mut self, component_type_path: Cow<'static, str>) -> &mut Self { pub fn remove_reflect(&mut self, component_type_path: Cow<'static, str>) -> &mut Self {
self.assert_not_despawned(); self.assert_not_despawned();
let entity_id = self.id(); self.resource_scope(|entity, registry: Mut<AppTypeRegistry>| {
self.world_scope(|world| {
world.resource_scope(|world, registry: Mut<AppTypeRegistry>| {
let type_registry = &registry.as_ref().read(); let type_registry = &registry.as_ref().read();
remove_reflect_with_registry_ref( remove_reflect_with_registry_ref(entity, type_registry, component_type_path);
world,
entity_id,
type_registry,
component_type_path,
);
}); });
world.flush();
});
self.update_location();
self self
} }
@ -313,34 +292,23 @@ impl<'w> EntityWorldMut<'w> {
/// # Panics /// # Panics
/// ///
/// - If the entity has been despawned while this `EntityWorldMut` is still alive. /// - If the entity has been despawned while this `EntityWorldMut` is still alive.
/// - If [`AppTypeRegistry`] is not present in the [`World`]. /// - If [`AppTypeRegistry`] is not present in the [`World`](crate::world::World).
pub fn remove_reflect_with_registry<T: Resource + AsRef<TypeRegistry>>( pub fn remove_reflect_with_registry<T: Resource + AsRef<TypeRegistry>>(
&mut self, &mut self,
component_type_path: Cow<'static, str>, component_type_path: Cow<'static, str>,
) -> &mut Self { ) -> &mut Self {
self.assert_not_despawned(); self.assert_not_despawned();
let entity_id = self.id(); self.resource_scope(|entity, registry: Mut<T>| {
self.world_scope(|world| {
world.resource_scope(|world, registry: Mut<T>| {
let type_registry = registry.as_ref().as_ref(); let type_registry = registry.as_ref().as_ref();
remove_reflect_with_registry_ref( remove_reflect_with_registry_ref(entity, type_registry, component_type_path);
world,
entity_id,
type_registry,
component_type_path,
);
}); });
world.flush();
});
self.update_location();
self self
} }
} }
/// Helper function to add a reflect component or bundle to a given entity /// Helper function to add a reflect component or bundle to a given entity
fn insert_reflect_with_registry_ref( fn insert_reflect_with_registry_ref(
world: &mut World, entity: &mut EntityWorldMut,
entity: Entity,
type_registry: &TypeRegistry, type_registry: &TypeRegistry,
component: Box<dyn PartialReflect>, component: Box<dyn PartialReflect>,
) { ) {
@ -348,18 +316,14 @@ fn insert_reflect_with_registry_ref(
.get_represented_type_info() .get_represented_type_info()
.expect("component should represent a type."); .expect("component should represent a type.");
let type_path = type_info.type_path(); let type_path = type_info.type_path();
let Ok(mut entity) = world.get_entity_mut(entity) else {
panic!("error[B0003]: Could not insert a reflected component (of type {type_path}) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003",
world.entities().entity_does_not_exist_error_details(entity));
};
let Some(type_registration) = type_registry.get(type_info.type_id()) else { let Some(type_registration) = type_registry.get(type_info.type_id()) else {
panic!("`{type_path}` should be registered in type registry via `App::register_type<{type_path}>`"); panic!("`{type_path}` should be registered in type registry via `App::register_type<{type_path}>`");
}; };
if let Some(reflect_component) = type_registration.data::<ReflectComponent>() { if let Some(reflect_component) = type_registration.data::<ReflectComponent>() {
reflect_component.insert(&mut entity, component.as_partial_reflect(), type_registry); reflect_component.insert(entity, component.as_partial_reflect(), type_registry);
} else if let Some(reflect_bundle) = type_registration.data::<ReflectBundle>() { } else if let Some(reflect_bundle) = type_registration.data::<ReflectBundle>() {
reflect_bundle.insert(&mut entity, component.as_partial_reflect(), type_registry); reflect_bundle.insert(entity, component.as_partial_reflect(), type_registry);
} else { } else {
panic!("`{type_path}` should have #[reflect(Component)] or #[reflect(Bundle)]"); panic!("`{type_path}` should have #[reflect(Component)] or #[reflect(Bundle)]");
} }
@ -367,21 +331,17 @@ fn insert_reflect_with_registry_ref(
/// Helper function to remove a reflect component or bundle from a given entity /// Helper function to remove a reflect component or bundle from a given entity
fn remove_reflect_with_registry_ref( fn remove_reflect_with_registry_ref(
world: &mut World, entity: &mut EntityWorldMut,
entity: Entity,
type_registry: &TypeRegistry, type_registry: &TypeRegistry,
component_type_path: Cow<'static, str>, component_type_path: Cow<'static, str>,
) { ) {
let Ok(mut entity) = world.get_entity_mut(entity) else {
return;
};
let Some(type_registration) = type_registry.get_with_type_path(&component_type_path) else { let Some(type_registration) = type_registry.get_with_type_path(&component_type_path) else {
return; return;
}; };
if let Some(reflect_component) = type_registration.data::<ReflectComponent>() { if let Some(reflect_component) = type_registration.data::<ReflectComponent>() {
reflect_component.remove(&mut entity); reflect_component.remove(entity);
} else if let Some(reflect_bundle) = type_registration.data::<ReflectBundle>() { } else if let Some(reflect_bundle) = type_registration.data::<ReflectBundle>() {
reflect_bundle.remove(&mut entity); reflect_bundle.remove(entity);
} }
} }

View File

@ -55,6 +55,8 @@ use variadics_please::{all_tuples, all_tuples_enumerated};
/// ///
/// ``` /// ```
/// # use bevy_ecs::prelude::*; /// # use bevy_ecs::prelude::*;
/// # #[derive(Component)]
/// # struct SomeComponent;
/// # #[derive(Resource)] /// # #[derive(Resource)]
/// # struct SomeResource; /// # struct SomeResource;
/// # #[derive(BufferedEvent)] /// # #[derive(BufferedEvent)]
@ -66,6 +68,8 @@ use variadics_please::{all_tuples, all_tuples_enumerated};
/// # struct ParamsExample<'w, 's> { /// # struct ParamsExample<'w, 's> {
/// # query: /// # query:
/// Query<'w, 's, Entity>, /// Query<'w, 's, Entity>,
/// # query2:
/// Query<'w, 's, &'static SomeComponent>,
/// # res: /// # res:
/// Res<'w, SomeResource>, /// Res<'w, SomeResource>,
/// # res_mut: /// # res_mut:

View File

@ -781,18 +781,18 @@ impl<'w> DeferredWorld<'w> {
/// Triggers all event observers for [`ComponentId`] in target. /// Triggers all event observers for [`ComponentId`] in target.
/// ///
/// # Safety /// # Safety
/// Caller must ensure observers listening for `event` can accept ZST pointers /// Caller must ensure observers listening for `event_key` can accept ZST pointers
#[inline] #[inline]
pub(crate) unsafe fn trigger_observers( pub(crate) unsafe fn trigger_observers(
&mut self, &mut self,
event: EventKey, event_key: EventKey,
target: Option<Entity>, target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone, components: impl Iterator<Item = ComponentId> + Clone,
caller: MaybeLocation, caller: MaybeLocation,
) { ) {
Observers::invoke::<_>( Observers::invoke::<_>(
self.reborrow(), self.reborrow(),
event, event_key,
target, target,
target, target,
components, components,
@ -805,11 +805,11 @@ impl<'w> DeferredWorld<'w> {
/// Triggers all event observers for [`ComponentId`] in target. /// Triggers all event observers for [`ComponentId`] in target.
/// ///
/// # Safety /// # Safety
/// Caller must ensure `E` is accessible as the type represented by `event` /// Caller must ensure `E` is accessible as the type represented by `event_key`
#[inline] #[inline]
pub(crate) unsafe fn trigger_observers_with_data<E, T>( pub(crate) unsafe fn trigger_observers_with_data<E, T>(
&mut self, &mut self,
event: EventKey, event_key: EventKey,
current_target: Option<Entity>, current_target: Option<Entity>,
original_target: Option<Entity>, original_target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone, components: impl Iterator<Item = ComponentId> + Clone,
@ -821,7 +821,7 @@ impl<'w> DeferredWorld<'w> {
{ {
Observers::invoke::<_>( Observers::invoke::<_>(
self.reborrow(), self.reborrow(),
event, event_key,
current_target, current_target,
original_target, original_target,
components.clone(), components.clone(),
@ -849,7 +849,7 @@ impl<'w> DeferredWorld<'w> {
} }
Observers::invoke::<_>( Observers::invoke::<_>(
self.reborrow(), self.reborrow(),
event, event_key,
Some(current_target), Some(current_target),
original_target, original_target,
components.clone(), components.clone(),

View File

@ -1547,6 +1547,50 @@ impl<'w> EntityWorldMut<'w> {
self.world.get_resource_mut() self.world.get_resource_mut()
} }
/// Temporarily removes the requested resource from the [`World`], runs custom user code,
/// then re-adds the resource before returning.
///
/// # Panics
///
/// Panics if the resource does not exist.
/// Use [`try_resource_scope`](Self::try_resource_scope) instead if you want to handle this case.
///
/// See [`World::resource_scope`] for further details.
#[track_caller]
pub fn resource_scope<R: Resource, U>(
&mut self,
f: impl FnOnce(&mut EntityWorldMut, Mut<R>) -> U,
) -> U {
let id = self.id();
self.world_scope(|world| {
world.resource_scope(|world, res| {
// Acquiring a new EntityWorldMut here and using that instead of `self` is fine because
// the outer `world_scope` will handle updating our location if it gets changed by the user code
let mut this = world.entity_mut(id);
f(&mut this, res)
})
})
}
/// Temporarily removes the requested resource from the [`World`] if it exists, runs custom user code,
/// then re-adds the resource before returning. Returns `None` if the resource does not exist in the [`World`].
///
/// See [`World::try_resource_scope`] for further details.
pub fn try_resource_scope<R: Resource, U>(
&mut self,
f: impl FnOnce(&mut EntityWorldMut, Mut<R>) -> U,
) -> Option<U> {
let id = self.id();
self.world_scope(|world| {
world.try_resource_scope(|world, res| {
// Acquiring a new EntityWorldMut here and using that instead of `self` is fine because
// the outer `world_scope` will handle updating our location if it gets changed by the user code
let mut this = world.entity_mut(id);
f(&mut this, res)
})
})
}
/// Retrieves the change ticks for the given component. This can be useful for implementing change /// Retrieves the change ticks for the given component. This can be useful for implementing change
/// detection in custom runtimes. /// detection in custom runtimes.
/// ///
@ -4959,6 +5003,46 @@ mod tests {
assert!(entity.get_mut_by_id(invalid_component_id).is_err()); assert!(entity.get_mut_by_id(invalid_component_id).is_err());
} }
#[derive(Resource)]
struct R(usize);
#[test]
fn entity_mut_resource_scope() {
// Keep in sync with the `resource_scope` test in lib.rs
let mut world = World::new();
let mut entity = world.spawn_empty();
assert!(entity.try_resource_scope::<R, _>(|_, _| {}).is_none());
entity.world_scope(|world| world.insert_resource(R(0)));
entity.resource_scope(|entity: &mut EntityWorldMut, mut value: Mut<R>| {
value.0 += 1;
assert!(!entity.world().contains_resource::<R>());
});
assert_eq!(entity.resource::<R>().0, 1);
}
#[test]
fn entity_mut_resource_scope_panic() {
let mut world = World::new();
world.insert_resource(R(0));
let mut entity = world.spawn_empty();
let old_location = entity.location();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
entity.resource_scope(|entity: &mut EntityWorldMut, _: Mut<R>| {
// Change the entity's `EntityLocation`.
entity.insert(TestComponent(0));
// Ensure that the entity location still gets updated even in case of a panic.
panic!("this should get caught by the outer scope")
});
}));
assert!(result.is_err());
// Ensure that the location has been properly updated.
assert_ne!(entity.location(), old_location);
}
// regression test for https://github.com/bevyengine/bevy/pull/7387 // regression test for https://github.com/bevyengine/bevy/pull/7387
#[test] #[test]
fn entity_mut_world_scope_panic() { fn entity_mut_world_scope_panic() {
@ -4983,6 +5067,28 @@ mod tests {
assert_ne!(entity.location(), old_location); assert_ne!(entity.location(), old_location);
} }
#[test]
fn entity_mut_reborrow_scope_panic() {
let mut world = World::new();
let mut entity = world.spawn_empty();
let old_location = entity.location();
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
entity.reborrow_scope(|mut entity| {
// Change the entity's `EntityLocation`, which invalidates the original `EntityWorldMut`.
// This will get updated at the end of the scope.
entity.insert(TestComponent(0));
// Ensure that the entity location still gets updated even in case of a panic.
panic!("this should get caught by the outer scope")
});
}));
assert!(res.is_err());
// Ensure that the location has been properly updated.
assert_ne!(entity.location(), old_location);
}
// regression test for https://github.com/bevyengine/bevy/pull/7805 // regression test for https://github.com/bevyengine/bevy/pull/7805
#[test] #[test]
fn removing_sparse_updates_archetype_row() { fn removing_sparse_updates_archetype_row() {
@ -5420,9 +5526,6 @@ mod tests {
#[derive(Component)] #[derive(Component)]
struct A; struct A;
#[derive(Resource)]
struct R;
#[test] #[test]
fn disjoint_access() { fn disjoint_access() {
fn disjoint_readonly(_: Query<EntityMut, With<A>>, _: Query<EntityRef, Without<A>>) {} fn disjoint_readonly(_: Query<EntityMut, With<A>>, _: Query<EntityRef, Without<A>>) {}

View File

@ -2551,6 +2551,11 @@ impl World {
/// This enables safe simultaneous mutable access to both a resource and the rest of the [`World`]. /// This enables safe simultaneous mutable access to both a resource and the rest of the [`World`].
/// For more complex access patterns, consider using [`SystemState`](crate::system::SystemState). /// For more complex access patterns, consider using [`SystemState`](crate::system::SystemState).
/// ///
/// # Panics
///
/// Panics if the resource does not exist.
/// Use [`try_resource_scope`](Self::try_resource_scope) instead if you want to handle this case.
///
/// # Example /// # Example
/// ``` /// ```
/// use bevy_ecs::prelude::*; /// use bevy_ecs::prelude::*;
@ -2568,8 +2573,6 @@ impl World {
/// }); /// });
/// assert_eq!(world.get_resource::<A>().unwrap().0, 2); /// assert_eq!(world.get_resource::<A>().unwrap().0, 2);
/// ``` /// ```
///
/// See also [`try_resource_scope`](Self::try_resource_scope).
#[track_caller] #[track_caller]
pub fn resource_scope<R: Resource, U>(&mut self, f: impl FnOnce(&mut World, Mut<R>) -> U) -> U { pub fn resource_scope<R: Resource, U>(&mut self, f: impl FnOnce(&mut World, Mut<R>) -> U) -> U {
self.try_resource_scope(f) self.try_resource_scope(f)

View File

@ -826,11 +826,26 @@ impl Mesh {
/// ///
/// # Errors /// # Errors
/// ///
/// Returns [`Err(MergeMeshError)`](MergeMeshError) if the vertex attribute values of `other` are incompatible with `self`. /// If any of the following conditions are not met, this function errors:
/// For example, [`VertexAttributeValues::Float32`] is incompatible with [`VertexAttributeValues::Float32x3`]. /// * All of the vertex attributes that have the same attribute id, must also
pub fn merge(&mut self, other: &Mesh) -> Result<(), MergeMeshError> { /// have the same attribute type.
/// For example two attributes with the same id, but where one is a
/// [`VertexAttributeValues::Float32`] and the other is a
/// [`VertexAttributeValues::Float32x3`], would be invalid.
/// * Both meshes must have the same primitive topology.
pub fn merge(&mut self, other: &Mesh) -> Result<(), MeshMergeError> {
use VertexAttributeValues::*; use VertexAttributeValues::*;
// Check if the meshes `primitive_topology` field is the same,
// as if that is not the case, the resulting mesh could (and most likely would)
// be invalid.
if self.primitive_topology != other.primitive_topology {
return Err(MeshMergeError::IncompatiblePrimitiveTopology {
self_primitive_topology: self.primitive_topology,
other_primitive_topology: other.primitive_topology,
});
}
// The indices of `other` should start after the last vertex of `self`. // The indices of `other` should start after the last vertex of `self`.
let index_offset = self.count_vertices(); let index_offset = self.count_vertices();
@ -871,7 +886,7 @@ impl Mesh {
(Uint8x4(vec1), Uint8x4(vec2)) => vec1.extend(vec2), (Uint8x4(vec1), Uint8x4(vec2)) => vec1.extend(vec2),
(Unorm8x4(vec1), Unorm8x4(vec2)) => vec1.extend(vec2), (Unorm8x4(vec1), Unorm8x4(vec2)) => vec1.extend(vec2),
_ => { _ => {
return Err(MergeMeshError { return Err(MeshMergeError::IncompatibleVertexAttributes {
self_attribute: *attribute, self_attribute: *attribute,
other_attribute: other other_attribute: other
.attribute_data(attribute.id) .attribute_data(attribute.id)
@ -1391,10 +1406,21 @@ impl MeshDeserializer {
/// Error that can occur when calling [`Mesh::merge`]. /// Error that can occur when calling [`Mesh::merge`].
#[derive(Error, Debug, Clone)] #[derive(Error, Debug, Clone)]
#[error("Incompatible vertex attribute types {} and {}", self_attribute.name, other_attribute.map(|a| a.name).unwrap_or("None"))] pub enum MeshMergeError {
pub struct MergeMeshError { #[error("Incompatible vertex attribute types: {} and {}", self_attribute.name, other_attribute.map(|a| a.name).unwrap_or("None"))]
pub self_attribute: MeshVertexAttribute, IncompatibleVertexAttributes {
pub other_attribute: Option<MeshVertexAttribute>, self_attribute: MeshVertexAttribute,
other_attribute: Option<MeshVertexAttribute>,
},
#[error(
"Incompatible primitive topologies: {:?} and {:?}",
self_primitive_topology,
other_primitive_topology
)]
IncompatiblePrimitiveTopology {
self_primitive_topology: PrimitiveTopology,
other_primitive_topology: PrimitiveTopology,
},
} }
#[cfg(test)] #[cfg(test)]

View File

@ -220,6 +220,21 @@ impl Timer {
self.duration = duration; self.duration = duration;
} }
/// Finishes the timer.
///
/// # Examples
/// ```
/// # use bevy_time::*;
/// let mut timer = Timer::from_seconds(1.5, TimerMode::Once);
/// timer.finish();
/// assert!(timer.finished());
/// ```
#[inline]
pub fn finish(&mut self) {
let remaining = self.remaining();
self.tick(remaining);
}
/// Returns the mode of the timer. /// Returns the mode of the timer.
/// ///
/// # Examples /// # Examples

View File

@ -44,37 +44,38 @@ impl BuildChildrenTransformExt for EntityCommands<'_> {
impl BuildChildrenTransformExt for EntityWorldMut<'_> { impl BuildChildrenTransformExt for EntityWorldMut<'_> {
fn set_parent_in_place(&mut self, parent: Entity) -> &mut Self { fn set_parent_in_place(&mut self, parent: Entity) -> &mut Self {
let child = self.id();
self.world_scope(|world| {
world.entity_mut(parent).add_child(child);
// FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436. // FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436.
let mut update_transform = || { let mut update_transform = || {
let parent = *world.get_entity(parent).ok()?.get::<GlobalTransform>()?; let child = self.id();
let child_global = *world.get_entity(child).ok()?.get::<GlobalTransform>()?; let parent_global = self.world_scope(|world| {
let mut child_entity = world.get_entity_mut(child).ok()?; world
let mut child = child_entity.get_mut::<Transform>()?; .get_entity_mut(parent)
*child = child_global.reparented_to(&parent); .ok()?
.add_child(child)
.get::<GlobalTransform>()
.copied()
})?;
let child_global = self.get::<GlobalTransform>()?;
let new_child_local = child_global.reparented_to(&parent_global);
let mut child_local = self.get_mut::<Transform>()?;
*child_local = new_child_local;
Some(()) Some(())
}; };
update_transform(); update_transform();
});
self self
} }
fn remove_parent_in_place(&mut self) -> &mut Self { fn remove_parent_in_place(&mut self) -> &mut Self {
let child = self.id(); self.remove::<ChildOf>();
self.world_scope(|world| {
world.entity_mut(child).remove::<ChildOf>();
// FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436. // FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436.
let mut update_transform = || { let mut update_transform = || {
let child_global = *world.get_entity(child).ok()?.get::<GlobalTransform>()?; let global = self.get::<GlobalTransform>()?;
let mut child_entity = world.get_entity_mut(child).ok()?; let new_local = global.compute_transform();
let mut child = child_entity.get_mut::<Transform>()?; let mut local = self.get_mut::<Transform>()?;
*child = child_global.compute_transform(); *local = new_local;
Some(()) Some(())
}; };
update_transform(); update_transform();
});
self self
} }
} }

View File

@ -7,7 +7,7 @@ use core::{
use super::shader_flags::BORDER_ALL; use super::shader_flags::BORDER_ALL;
use crate::*; use crate::*;
use bevy_asset::*; use bevy_asset::*;
use bevy_color::{ColorToComponents, LinearRgba}; use bevy_color::{ColorToComponents, Hsla, Hsva, LinearRgba, Oklaba, Oklcha, Srgba};
use bevy_ecs::{ use bevy_ecs::{
prelude::Component, prelude::Component,
system::{ system::{
@ -654,6 +654,44 @@ struct UiGradientVertex {
hint: f32, hint: f32,
} }
fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> [f32; 4] {
match space {
InterpolationColorSpace::Oklaba => {
let oklaba: Oklaba = color.into();
[oklaba.lightness, oklaba.a, oklaba.b, oklaba.alpha]
}
InterpolationColorSpace::Oklcha | InterpolationColorSpace::OklchaLong => {
let oklcha: Oklcha = color.into();
[
oklcha.lightness,
oklcha.chroma,
oklcha.hue.to_radians(),
oklcha.alpha,
]
}
InterpolationColorSpace::Srgba => {
let srgba: Srgba = color.into();
[srgba.red, srgba.green, srgba.blue, srgba.alpha]
}
InterpolationColorSpace::LinearRgba => color.to_f32_array(),
InterpolationColorSpace::Hsla | InterpolationColorSpace::HslaLong => {
let hsla: Hsla = color.into();
// Normalize hue to 0..1 range for shader
[
hsla.hue / 360.0,
hsla.saturation,
hsla.lightness,
hsla.alpha,
]
}
InterpolationColorSpace::Hsva | InterpolationColorSpace::HsvaLong => {
let hsva: Hsva = color.into();
// Normalize hue to 0..1 range for shader
[hsva.hue / 360.0, hsva.saturation, hsva.value, hsva.alpha]
}
}
}
pub fn prepare_gradient( pub fn prepare_gradient(
mut commands: Commands, mut commands: Commands,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
@ -804,8 +842,9 @@ pub fn prepare_gradient(
continue; continue;
} }
} }
let start_color = start_stop.0.to_f32_array(); let start_color =
let end_color = end_stop.0.to_f32_array(); convert_color_to_space(start_stop.0, gradient.color_space);
let end_color = convert_color_to_space(end_stop.0, gradient.color_space);
let mut stop_flags = flags; let mut stop_flags = flags;
if 0. < start_stop.1 if 0. < start_stop.1
&& (stop_index == gradient.stops_range.start || segment_count == 0) && (stop_index == gradient.stops_range.start || segment_count == 0)

View File

@ -114,26 +114,6 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4<f32> {
} }
} }
// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space.
fn mix_linear_rgba_in_srgba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let a_srgb = pow(a.rgb, vec3(1. / 2.2));
let b_srgb = pow(b.rgb, vec3(1. / 2.2));
let mixed_srgb = mix(a_srgb, b_srgb, t);
return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t));
}
fn linear_rgba_to_oklaba(c: vec4<f32>) -> vec4<f32> {
let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.);
let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.);
let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.);
return vec4(
0.21045426 * l + 0.7936178 * m - 0.004072047 * s,
1.9779985 * l - 2.4285922 * m + 0.4505937 * s,
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
c.a
);
}
fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> { fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z; let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z;
let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z; let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z;
@ -149,33 +129,6 @@ fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
); );
} }
fn mix_linear_rgba_in_oklaba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t));
}
fn linear_rgba_to_hsla(c: vec4<f32>) -> vec4<f32> {
let max = max(max(c.r, c.g), c.b);
let min = min(min(c.r, c.g), c.b);
let l = (max + min) * 0.5;
if max == min {
return vec4(0., 0., l, c.a);
} else {
let delta = max - min;
let s = delta / (1. - abs(2. * l - 1.));
var h = 0.;
if max == c.r {
h = ((c.g - c.b) / delta) % 6.;
} else if max == c.g {
h = ((c.b - c.r) / delta) + 2.;
} else {
h = ((c.r - c.g) / delta) + 4.;
}
h = h / 6.;
return vec4<f32>(h, s, l, c.a);
}
}
fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> { fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
let h = hsl.x; let h = hsl.x;
let s = hsl.y; let s = hsl.y;
@ -203,30 +156,6 @@ fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
return vec4<f32>(r + m, g + m, b + m, hsl.a); return vec4<f32>(r + m, g + m, b + m, hsl.a);
} }
fn linear_rgba_to_hsva(c: vec4<f32>) -> vec4<f32> {
let maxc = max(max(c.r, c.g), c.b);
let minc = min(min(c.r, c.g), c.b);
let delta = maxc - minc;
var h: f32 = 0.0;
var s: f32 = 0.0;
let v: f32 = maxc;
if delta != 0.0 {
s = delta / maxc;
if maxc == c.r {
h = ((c.g - c.b) / delta) % 6.0;
} else if maxc == c.g {
h = ((c.b - c.r) / delta) + 2.0;
} else {
h = ((c.r - c.g) / delta) + 4.0;
}
h = h / 6.0;
if h < 0.0 {
h = h + 1.0;
}
}
return vec4<f32>(h, s, v, c.a);
}
fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> { fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
let h = hsva.x * 6.0; let h = hsva.x * 6.0;
let s = hsva.y; let s = hsva.y;
@ -253,14 +182,6 @@ fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
return vec4<f32>(r + m, g + m, b + m, hsva.a); return vec4<f32>(r + m, g + m, b + m, hsva.a);
} }
/// hue is left in radians and not converted to degrees
fn linear_rgba_to_oklcha(c: vec4<f32>) -> vec4<f32> {
let o = linear_rgba_to_oklaba(c);
let chroma = sqrt(o.y * o.y + o.z * o.z);
let hue = atan2(o.z, o.y);
return vec4(o.x, chroma, rem_euclid(hue, TAU), o.a);
}
fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> { fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
let a = c.y * cos(c.z); let a = c.y * cos(c.z);
let b = c.y * sin(c.z); let b = c.y * sin(c.z);
@ -271,90 +192,6 @@ fn rem_euclid(a: f32, b: f32) -> f32 {
return ((a % b) + b) % b; return ((a % b) + b) % b;
} }
fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
let diff = rem_euclid(b - a + PI, TAU) - PI;
return rem_euclid(a + diff * t, TAU);
}
fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 {
let diff = rem_euclid(b - a + PI, TAU) - PI;
return rem_euclid(a + (diff + select(TAU, -TAU, 0. < diff)) * t, TAU);
}
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ah = select(a.z, b.z, a.y == 0.);
let bh = select(b.z, a.z, b.y == 0.);
return vec4(
mix(a.xy, b.xy, t),
lerp_hue(ah, bh, t),
mix(a.a, b.a, t)
);
}
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ah = select(a.z, b.z, a.y == 0.);
let bh = select(b.z, a.z, b.y == 0.);
return vec4(
mix(a.xy, b.xy, t),
lerp_hue_long(ah, bh, t),
mix(a.w, b.w, t)
);
}
fn mix_linear_rgba_in_oklcha_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgba_in_oklcha_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgba_in_hsva_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
var h: f32;
if ha.y == 0. {
h = hb.x;
} else if hb.y == 0. {
h = ha.x;
} else {
h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
}
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsva_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsla_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
fn mix_linear_rgba_in_hsla_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
// These functions are used to calculate the distance in gradient space from the start of the gradient to the point. // These functions are used to calculate the distance in gradient space from the start of the gradient to the point.
// The distance in gradient space is then used to interpolate between the start and end colors. // The distance in gradient space is then used to interpolate between the start and end colors.
@ -386,6 +223,105 @@ fn conic_distance(
return (((angle - start) % TAU) + TAU) % TAU; return (((angle - start) % TAU) + TAU) % TAU;
} }
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.z - a.z;
var adjusted_hue = a.z;
if abs(hue_diff) > PI {
if hue_diff > 0.0 {
adjusted_hue = a.z + (hue_diff - TAU) * t;
} else {
adjusted_hue = a.z + (hue_diff + TAU) * t;
}
} else {
adjusted_hue = a.z + hue_diff * t;
}
return vec4(
mix(a.x, b.x, t),
mix(a.y, b.y, t),
rem_euclid(adjusted_hue, TAU),
mix(a.w, b.w, t)
);
}
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.z - a.z;
var adjusted_hue = a.z;
if abs(hue_diff) < PI {
if hue_diff >= 0.0 {
adjusted_hue = a.z + (hue_diff - TAU) * t;
} else {
adjusted_hue = a.z + (hue_diff + TAU) * t;
}
} else {
adjusted_hue = a.z + hue_diff * t;
}
return vec4(
mix(a.x, b.x, t),
mix(a.y, b.y, t),
rem_euclid(adjusted_hue, TAU),
mix(a.w, b.w, t)
);
}
fn mix_hsla(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return vec4(
fract(a.x + (fract(b.x - a.x + 0.5) - 0.5) * t),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsla_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let d = fract(b.x - a.x + 0.5) - 0.5;
return vec4(
fract(a.x + (d + select(1., -1., 0. < d)) * t),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsva(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) > 0.5 {
if hue_diff > 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsva_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) < 0.5 {
if hue_diff >= 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn interpolate_gradient( fn interpolate_gradient(
distance: f32, distance: f32,
start_color: vec4<f32>, start_color: vec4<f32>,
@ -397,10 +333,10 @@ fn interpolate_gradient(
) -> vec4<f32> { ) -> vec4<f32> {
if start_distance == end_distance { if start_distance == end_distance {
if distance <= start_distance && enabled(flags, FILL_START) { if distance <= start_distance && enabled(flags, FILL_START) {
return start_color; return convert_to_linear_rgba(start_color);
} }
if start_distance <= distance && enabled(flags, FILL_END) { if start_distance <= distance && enabled(flags, FILL_END) {
return end_color; return convert_to_linear_rgba(end_color);
} }
return vec4(0.); return vec4(0.);
} }
@ -409,14 +345,14 @@ fn interpolate_gradient(
if t < 0.0 { if t < 0.0 {
if enabled(flags, FILL_START) { if enabled(flags, FILL_START) {
return start_color; return convert_to_linear_rgba(start_color);
} }
return vec4(0.0); return vec4(0.0);
} }
if 1. < t { if 1. < t {
if enabled(flags, FILL_END) { if enabled(flags, FILL_END) {
return end_color; return convert_to_linear_rgba(end_color);
} }
return vec4(0.0); return vec4(0.0);
} }
@ -427,23 +363,55 @@ fn interpolate_gradient(
t = 0.5 * (1 + (t - hint) / (1.0 - hint)); t = 0.5 * (1 + (t - hint) / (1.0 - hint));
} }
#ifdef IN_SRGB return convert_to_linear_rgba(mix_colors(start_color, end_color, t));
return mix_linear_rgba_in_srgba_space(start_color, end_color, t); }
#else ifdef IN_OKLAB
return mix_linear_rgba_in_oklaba_space(start_color, end_color, t); // Mix the colors, choosing the appropriate interpolation method for the given color space
#else ifdef IN_OKLCH fn mix_colors(
return mix_linear_rgba_in_oklcha_space(start_color, end_color, t); start_color: vec4<f32>,
end_color: vec4<f32>,
t: f32,
) -> vec4<f32> {
#ifdef IN_OKLCH
return mix_oklcha(start_color, end_color, t);
#else ifdef IN_OKLCH_LONG #else ifdef IN_OKLCH_LONG
return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t); return mix_oklcha_long(start_color, end_color, t);
#else ifdef IN_HSV #else ifdef IN_HSV
return mix_linear_rgba_in_hsva_space(start_color, end_color, t); return mix_hsva(start_color, end_color, t);
#else ifdef IN_HSV_LONG #else ifdef IN_HSV_LONG
return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t); return mix_hsva_long(start_color, end_color, t);
#else ifdef IN_HSL #else ifdef IN_HSL
return mix_linear_rgba_in_hsla_space(start_color, end_color, t); return mix_hsla(start_color, end_color, t);
#else ifdef IN_HSL_LONG #else ifdef IN_HSL_LONG
return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t); return mix_hsla_long(start_color, end_color, t);
#else #else
// Just lerp in linear RGBA, OkLab and SRGBA spaces
return mix(start_color, end_color, t); return mix(start_color, end_color, t);
#endif #endif
} }
// Convert a color from the interpolation color space to linear rgba
fn convert_to_linear_rgba(
color: vec4<f32>,
) -> vec4<f32> {
#ifdef IN_OKLCH
return oklcha_to_linear_rgba(color);
#else ifdef IN_OKLCH_LONG
return oklcha_to_linear_rgba(color);
#else ifdef IN_HSV
return hsva_to_linear_rgba(color);
#else ifdef IN_HSV_LONG
return hsva_to_linear_rgba(color);
#else ifdef IN_HSL
return hsla_to_linear_rgba(color);
#else ifdef IN_HSL_LONG
return hsla_to_linear_rgba(color);
#else ifdef IN_OKLAB
return oklaba_to_linear_rgba(color);
#else ifdef IN_SRGB
return vec4(pow(color.rgb, vec3(2.2)), color.a);
#else
// Color is already in linear rgba space
return color;
#endif
}

View File

@ -540,14 +540,16 @@ fn process_scale_input(
for (mut transform, selection) in &mut scale_selections { for (mut transform, selection) in &mut scale_selections {
if app_status.selection == *selection { if app_status.selection == *selection {
transform.scale *= 1.0 + mouse_motion.delta.x * SCALE_SPEED; transform.scale = (transform.scale * (1.0 + mouse_motion.delta.x * SCALE_SPEED))
.clamp(Vec3::splat(0.01), Vec3::splat(5.0));
} }
} }
for (mut spotlight, selection) in &mut spotlight_selections { for (mut spotlight, selection) in &mut spotlight_selections {
if app_status.selection == *selection { if app_status.selection == *selection {
spotlight.outer_angle = spotlight.outer_angle = (spotlight.outer_angle
(spotlight.outer_angle * (1.0 + mouse_motion.delta.x * SCALE_SPEED)).min(FRAC_PI_4); * (1.0 + mouse_motion.delta.x * SCALE_SPEED))
.clamp(0.01, FRAC_PI_4);
spotlight.inner_angle = spotlight.outer_angle; spotlight.inner_angle = spotlight.outer_angle;
} }
} }

View File

@ -510,6 +510,7 @@ Example | Description
[Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000 [Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000
[Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos [Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos
[Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering. [Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering.
[Many Gradients](../examples/stress_tests/many_gradients.rs) | Stress test for gradient rendering performance
[Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights [Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights
[Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites. [Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites.
[Many Text2d](../examples/stress_tests/many_text2d.rs) | Displays many Text2d! Used for performance testing. [Many Text2d](../examples/stress_tests/many_text2d.rs) | Displays many Text2d! Used for performance testing.

View File

@ -0,0 +1,180 @@
//! Stress test demonstrating gradient performance improvements.
//!
//! This example creates many UI nodes with gradients to measure the performance
//! impact of pre-converting colors to the target color space on the CPU.
use argh::FromArgs;
use bevy::{
color::palettes::css::*,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
math::ops::sin,
prelude::*,
ui::{
BackgroundGradient, ColorStop, Display, Gradient, InterpolationColorSpace, LinearGradient,
RepeatedGridTrack,
},
window::{PresentMode, WindowResolution},
};
const COLS: usize = 30;
#[derive(FromArgs, Resource, Debug)]
/// Gradient stress test
struct Args {
/// how many gradients per group (default: 900)
#[argh(option, default = "900")]
gradient_count: usize,
/// whether to animate gradients by changing colors
#[argh(switch)]
animate: bool,
/// use sRGB interpolation
#[argh(switch)]
srgb: bool,
/// use HSL interpolation
#[argh(switch)]
hsl: bool,
}
fn main() {
let args: Args = argh::from_env();
let total_gradients = args.gradient_count;
println!("Gradient stress test with {total_gradients} gradients");
println!(
"Color space: {}",
if args.srgb {
"sRGB"
} else if args.hsl {
"HSL"
} else {
"OkLab (default)"
}
);
App::new()
.add_plugins((
LogDiagnosticsPlugin::default(),
FrameTimeDiagnosticsPlugin::default(),
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Gradient Stress Test".to_string(),
resolution: WindowResolution::new(1920.0, 1080.0),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
))
.insert_resource(args)
.add_systems(Startup, setup)
.add_systems(Update, animate_gradients)
.run();
}
fn setup(mut commands: Commands, args: Res<Args>) {
commands.spawn(Camera2d);
let rows_to_spawn = args.gradient_count.div_ceil(COLS);
// Create a grid of gradients
commands
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
display: Display::Grid,
grid_template_columns: RepeatedGridTrack::flex(COLS as u16, 1.0),
grid_template_rows: RepeatedGridTrack::flex(rows_to_spawn as u16, 1.0),
..default()
})
.with_children(|parent| {
for i in 0..args.gradient_count {
let angle = (i as f32 * 10.0) % 360.0;
let mut gradient = LinearGradient::new(
angle,
vec![
ColorStop::new(RED, Val::Percent(0.0)),
ColorStop::new(BLUE, Val::Percent(100.0)),
ColorStop::new(GREEN, Val::Percent(20.0)),
ColorStop::new(YELLOW, Val::Percent(40.0)),
ColorStop::new(ORANGE, Val::Percent(60.0)),
ColorStop::new(LIME, Val::Percent(80.0)),
ColorStop::new(DARK_CYAN, Val::Percent(90.0)),
],
);
gradient.color_space = if args.srgb {
InterpolationColorSpace::Srgba
} else if args.hsl {
InterpolationColorSpace::Hsla
} else {
InterpolationColorSpace::Oklaba
};
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
BackgroundGradient(vec![Gradient::Linear(gradient)]),
GradientNode { index: i },
));
}
});
}
#[derive(Component)]
struct GradientNode {
index: usize,
}
fn animate_gradients(
mut gradients: Query<(&mut BackgroundGradient, &GradientNode)>,
args: Res<Args>,
time: Res<Time>,
) {
if !args.animate {
return;
}
let t = time.elapsed_secs();
for (mut bg_gradient, node) in &mut gradients {
let offset = node.index as f32 * 0.01;
let hue_shift = sin(t + offset) * 0.5 + 0.5;
if let Some(Gradient::Linear(gradient)) = bg_gradient.0.get_mut(0) {
let color1 = Color::hsl(hue_shift * 360.0, 1.0, 0.5);
let color2 = Color::hsl((hue_shift + 0.3) * 360.0 % 360.0, 1.0, 0.5);
gradient.stops = vec![
ColorStop::new(color1, Val::Percent(0.0)),
ColorStop::new(color2, Val::Percent(100.0)),
ColorStop::new(
Color::hsl((hue_shift + 0.1) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(20.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.15) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(40.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.2) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(60.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.25) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(80.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.28) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(90.0),
),
];
}
}
}

View File

@ -0,0 +1,10 @@
---
title: Rework `MergeMeshError`
pull_requests: [18561]
---
`MergeMeshError` was reworked to account for the possibility of the meshes being merged having two different `PrimitiveTopology`'s, and was renamed to `MeshMergeError` to align with the naming of other mesh errors.
- Users will need to rename `MergeMeshError` to `MeshMergeError`
- When handling `MergeMeshError` (now `MeshMergeError`), users will need to account for the new `IncompatiblePrimitiveTopology` variant, as it has been changed from a struct to an enum
- `Mesh::merge` now returns `Result<(), MeshMergeError>` instead of the previous `Result<(), MergeMeshError>`