Merge branch 'main' into transmission

This commit is contained in:
Marco Buono 2023-07-26 17:49:40 -03:00
commit eb41464ff9
51 changed files with 879 additions and 298 deletions

View file

@ -29,11 +29,36 @@ jobs:
check-bans: check-bans:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# on main, prepare a new cargo tree output and cache it
- name: On main, prepare new cargo tree cache
if: github.ref == 'refs/heads/main'
run: cargo tree --depth 3 > cargo-tree-from-main
- name: On main, save the new cargo tree cache
if: github.ref == 'refs/heads/main'
uses: actions/cache/save@v3
with:
path: cargo-tree-from-main
key: cargo-tree-from-main
# on other branch, restore the cached cargo tree output
- name: On PR, restore cargo tree cache
uses: actions/cache/restore@v3
if: github.ref != 'refs/heads/main'
with:
path: cargo-tree-from-main
key: cargo-tree-from-main
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
# if not on main, check that the cargo tree output is unchanged
- name: Check if the cargo tree changed from main
if: github.ref != 'refs/heads/main'
continue-on-error: true
id: cargo-tree-changed
run: diff cargo-tree-from-main <(cargo tree --depth 3)
- name: Install cargo-deny - name: Install cargo-deny
run: cargo install cargo-deny run: cargo install cargo-deny
# if the check was not a success (either skipped because on main or failed because of a change), run the check
- name: Check for banned and duplicated dependencies - name: Check for banned and duplicated dependencies
if: steps.cargo-tree-changed.outcome != 'success'
run: cargo deny check bans run: cargo deny check bans
check-licenses: check-licenses:

View file

@ -34,7 +34,7 @@ pub fn heavy_compute(c: &mut Criterion) {
})); }));
fn sys(mut query: Query<(&mut Position, &mut Transform)>) { fn sys(mut query: Query<(&mut Position, &mut Transform)>) {
query.par_iter_mut().for_each_mut(|(mut pos, mut mat)| { query.par_iter_mut().for_each(|(mut pos, mut mat)| {
for _ in 0..100 { for _ in 0..100 {
mat.0 = mat.0.inverse(); mat.0 = mat.0.inverse();
} }

View file

@ -364,7 +364,7 @@ pub fn animation_player(
) { ) {
animation_players animation_players
.par_iter_mut() .par_iter_mut()
.for_each_mut(|(root, maybe_parent, mut player)| { .for_each(|(root, maybe_parent, mut player)| {
update_transitions(&mut player, &time); update_transitions(&mut player, &time);
run_animation_player( run_animation_player(
root, root,

View file

@ -686,6 +686,7 @@ impl App {
/// Panics if one of the plugins was already added to the application. /// Panics if one of the plugins was already added to the application.
/// ///
/// [`PluginGroup`]:super::PluginGroup /// [`PluginGroup`]:super::PluginGroup
#[track_caller]
pub fn add_plugins<M>(&mut self, plugins: impl Plugins<M>) -> &mut Self { pub fn add_plugins<M>(&mut self, plugins: impl Plugins<M>) -> &mut Self {
plugins.add_to_app(self); plugins.add_to_app(self);
self self

View file

@ -89,6 +89,7 @@ mod sealed {
pub struct PluginsTupleMarker; pub struct PluginsTupleMarker;
impl<P: Plugin> Plugins<PluginMarker> for P { impl<P: Plugin> Plugins<PluginMarker> for P {
#[track_caller]
fn add_to_app(self, app: &mut App) { fn add_to_app(self, app: &mut App) {
if let Err(AppError::DuplicatePlugin { plugin_name }) = if let Err(AppError::DuplicatePlugin { plugin_name }) =
app.add_boxed_plugin(Box::new(self)) app.add_boxed_plugin(Box::new(self))
@ -101,6 +102,7 @@ mod sealed {
} }
impl<P: PluginGroup> Plugins<PluginGroupMarker> for P { impl<P: PluginGroup> Plugins<PluginGroupMarker> for P {
#[track_caller]
fn add_to_app(self, app: &mut App) { fn add_to_app(self, app: &mut App) {
self.build().finish(app); self.build().finish(app);
} }
@ -113,6 +115,7 @@ mod sealed {
$($plugins: Plugins<$param>),* $($plugins: Plugins<$param>),*
{ {
#[allow(non_snake_case, unused_variables)] #[allow(non_snake_case, unused_variables)]
#[track_caller]
fn add_to_app(self, app: &mut App) { fn add_to_app(self, app: &mut App) {
let ($($plugins,)*) = self; let ($($plugins,)*) = self;
$($plugins.add_to_app(app);)* $($plugins.add_to_app(app);)*

View file

@ -172,6 +172,7 @@ impl PluginGroupBuilder {
/// # Panics /// # Panics
/// ///
/// Panics if one of the plugin in the group was already added to the application. /// Panics if one of the plugin in the group was already added to the application.
#[track_caller]
pub fn finish(mut self, app: &mut App) { pub fn finish(mut self, app: &mut App) {
for ty in &self.order { for ty in &self.order {
if let Some(entry) = self.plugins.remove(ty) { if let Some(entry) = self.plugins.remove(ty) {

View file

@ -237,6 +237,16 @@ pub fn derive_world_query_impl(input: TokenStream) -> TokenStream {
#marker_name: &'__w (), #marker_name: &'__w (),
} }
impl #user_impl_generics_with_world Clone for #fetch_struct_name #user_ty_generics_with_world
#user_where_clauses_with_world {
fn clone(&self) -> Self {
Self {
#(#named_field_idents: self.#named_field_idents.clone(),)*
#marker_name: &(),
}
}
}
// SAFETY: `update_component_access` and `update_archetype_component_access` are called on every field // SAFETY: `update_component_access` and `update_archetype_component_access` are called on every field
unsafe impl #user_impl_generics #path::query::WorldQuery unsafe impl #user_impl_generics #path::query::WorldQuery
for #struct_name #user_ty_generics #user_where_clauses { for #struct_name #user_ty_generics #user_where_clauses {
@ -275,17 +285,6 @@ pub fn derive_world_query_impl(input: TokenStream) -> TokenStream {
} }
} }
unsafe fn clone_fetch<'__w>(
_fetch: &<Self as #path::query::WorldQuery>::Fetch<'__w>
) -> <Self as #path::query::WorldQuery>::Fetch<'__w> {
#fetch_struct_name {
#(
#named_field_idents: <#field_types>::clone_fetch(& _fetch. #named_field_idents),
)*
#marker_name: &(),
}
}
const IS_DENSE: bool = true #(&& <#field_types>::IS_DENSE)*; const IS_DENSE: bool = true #(&& <#field_types>::IS_DENSE)*;
const IS_ARCHETYPAL: bool = true #(&& <#field_types>::IS_ARCHETYPAL)*; const IS_ARCHETYPAL: bool = true #(&& <#field_types>::IS_ARCHETYPAL)*;

View file

@ -735,6 +735,35 @@ impl<'a> MutUntyped<'a> {
self.value.as_ref() self.value.as_ref()
} }
/// Turn this [`MutUntyped`] into a [`Mut`] by mapping the inner [`PtrMut`] to another value,
/// without flagging a change.
/// This function is the untyped equivalent of [`Mut::map_unchanged`].
///
/// You should never modify the argument passed to the closure if you want to modify the data without flagging a change, consider using [`bypass_change_detection`](DetectChangesMut::bypass_change_detection) to make your intent explicit.
///
/// If you know the type of the value you can do
/// ```no_run
/// # use bevy_ecs::change_detection::{Mut, MutUntyped};
/// # let mut_untyped: MutUntyped = unimplemented!();
/// // SAFETY: ptr is of type `u8`
/// mut_untyped.map_unchanged(|ptr| unsafe { ptr.deref_mut::<u8>() });
/// ```
/// If you have a [`ReflectFromPtr`](bevy_reflect::ReflectFromPtr) that you know belongs to this [`MutUntyped`],
/// you can do
/// ```no_run
/// # use bevy_ecs::change_detection::{Mut, MutUntyped};
/// # let mut_untyped: MutUntyped = unimplemented!();
/// # let reflect_from_ptr: bevy_reflect::ReflectFromPtr = unimplemented!();
/// // SAFETY: from the context it is known that `ReflectFromPtr` was made for the type of the `MutUntyped`
/// mut_untyped.map_unchanged(|ptr| unsafe { reflect_from_ptr.as_reflect_ptr_mut(ptr) });
/// ```
pub fn map_unchanged<T: ?Sized>(self, f: impl FnOnce(PtrMut<'a>) -> &'a mut T) -> Mut<'a, T> {
Mut {
value: f(self.value),
ticks: self.ticks,
}
}
/// Transforms this [`MutUntyped`] into a [`Mut<T>`] with the same lifetime. /// Transforms this [`MutUntyped`] into a [`Mut<T>`] with the same lifetime.
/// ///
/// # Safety /// # Safety
@ -798,6 +827,8 @@ impl std::fmt::Debug for MutUntyped<'_> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bevy_ecs_macros::Resource; use bevy_ecs_macros::Resource;
use bevy_ptr::PtrMut;
use bevy_reflect::{FromType, ReflectFromPtr};
use crate::{ use crate::{
self as bevy_ecs, self as bevy_ecs,
@ -809,8 +840,7 @@ mod tests {
world::World, world::World,
}; };
use super::DetectChanges; use super::{DetectChanges, DetectChangesMut, MutUntyped};
use super::DetectChangesMut;
#[derive(Component, PartialEq)] #[derive(Component, PartialEq)]
struct C; struct C;
@ -1034,4 +1064,40 @@ mod tests {
"Resource must be changed after setting to a different value." "Resource must be changed after setting to a different value."
); );
} }
#[test]
fn mut_untyped_to_reflect() {
let last_run = Tick::new(2);
let this_run = Tick::new(3);
let mut component_ticks = ComponentTicks {
added: Tick::new(1),
changed: Tick::new(2),
};
let ticks = TicksMut {
added: &mut component_ticks.added,
changed: &mut component_ticks.changed,
last_run,
this_run,
};
let mut value: i32 = 5;
let value = MutUntyped {
value: PtrMut::from(&mut value),
ticks,
};
let reflect_from_ptr = <ReflectFromPtr as FromType<i32>>::from_type();
let mut new = value.map_unchanged(|ptr| {
// SAFETY: The underlying type of `ptr` matches `reflect_from_ptr`.
let value = unsafe { reflect_from_ptr.as_reflect_ptr_mut(ptr) };
value
});
assert!(!new.is_changed());
new.reflect_mut();
assert!(new.is_changed());
}
} }

View file

@ -37,7 +37,7 @@ pub mod prelude {
component::Component, component::Component,
entity::Entity, entity::Entity,
event::{Event, EventReader, EventWriter, Events}, event::{Event, EventReader, EventWriter, Events},
query::{Added, AnyOf, Changed, Or, QueryState, With, Without}, query::{Added, AnyOf, Changed, Has, Or, QueryState, With, Without},
removal_detection::RemovedComponents, removal_detection::RemovedComponents,
schedule::{ schedule::{
apply_deferred, apply_state_transition, common_conditions::*, Condition, apply_deferred, apply_state_transition, common_conditions::*, Condition,

View file

@ -318,7 +318,7 @@ pub unsafe trait WorldQuery {
type Item<'a>; type Item<'a>;
/// Per archetype/table state used by this [`WorldQuery`] to fetch [`Self::Item`](crate::query::WorldQuery::Item) /// Per archetype/table state used by this [`WorldQuery`] to fetch [`Self::Item`](crate::query::WorldQuery::Item)
type Fetch<'a>; type Fetch<'a>: Clone;
/// The read-only variant of this [`WorldQuery`], which satisfies the [`ReadOnlyWorldQuery`] trait. /// The read-only variant of this [`WorldQuery`], which satisfies the [`ReadOnlyWorldQuery`] trait.
type ReadOnly: ReadOnlyWorldQuery<State = Self::State>; type ReadOnly: ReadOnlyWorldQuery<State = Self::State>;
@ -345,14 +345,6 @@ pub unsafe trait WorldQuery {
this_run: Tick, this_run: Tick,
) -> Self::Fetch<'w>; ) -> Self::Fetch<'w>;
/// While this function can be called for any query, it is always safe to call if `Self: ReadOnlyWorldQuery` holds.
///
/// # Safety
/// While calling this method on its own cannot cause UB it is marked `unsafe` as the caller must ensure
/// that the returned value is not used in any way that would cause two `QueryItem<Self>` for the same
/// `archetype_row` or `table_row` to be alive at the same time.
unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w>;
/// Returns true if (and only if) every table of every archetype matched by this fetch contains /// Returns true if (and only if) every table of every archetype matched by this fetch contains
/// all of the matched components. This is used to select a more efficient "table iterator" /// all of the matched components. This is used to select a more efficient "table iterator"
/// for "dense" queries. If this returns true, [`WorldQuery::set_table`] must be used before /// for "dense" queries. If this returns true, [`WorldQuery::set_table`] must be used before
@ -404,6 +396,10 @@ pub unsafe trait WorldQuery {
/// ///
/// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and
/// `table_row` must be in the range of the current table and archetype. /// `table_row` must be in the range of the current table and archetype.
///
/// If `update_component_access` includes any mutable accesses, then the caller must ensure
/// that `fetch` is called no more than once for each `entity`/`table_row` in each archetype.
/// If `Self` implements [`ReadOnlyWorldQuery`], then this can safely be called multiple times.
unsafe fn fetch<'w>( unsafe fn fetch<'w>(
fetch: &mut Self::Fetch<'w>, fetch: &mut Self::Fetch<'w>,
entity: Entity, entity: Entity,
@ -483,8 +479,6 @@ unsafe impl WorldQuery for Entity {
) -> Self::Fetch<'w> { ) -> Self::Fetch<'w> {
} }
unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {}
#[inline] #[inline]
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w>(
_fetch: &mut Self::Fetch<'w>, _fetch: &mut Self::Fetch<'w>,
@ -554,10 +548,6 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> {
world.world() world.world()
} }
unsafe fn clone_fetch<'w>(world: &Self::Fetch<'w>) -> Self::Fetch<'w> {
world
}
#[inline] #[inline]
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w>(
_fetch: &mut Self::Fetch<'w>, _fetch: &mut Self::Fetch<'w>,
@ -620,6 +610,13 @@ pub struct ReadFetch<'w, T> {
sparse_set: Option<&'w ComponentSparseSet>, sparse_set: Option<&'w ComponentSparseSet>,
} }
impl<T> Clone for ReadFetch<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ReadFetch<'_, T> {}
/// SAFETY: `Self` is the same as `Self::ReadOnly` /// SAFETY: `Self` is the same as `Self::ReadOnly`
unsafe impl<T: Component> WorldQuery for &T { unsafe impl<T: Component> WorldQuery for &T {
type Fetch<'w> = ReadFetch<'w, T>; type Fetch<'w> = ReadFetch<'w, T>;
@ -663,13 +660,6 @@ unsafe impl<T: Component> WorldQuery for &T {
} }
} }
unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {
ReadFetch {
table_components: fetch.table_components,
sparse_set: fetch.sparse_set,
}
}
#[inline] #[inline]
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w>(
fetch: &mut ReadFetch<'w, T>, fetch: &mut ReadFetch<'w, T>,
@ -770,6 +760,13 @@ pub struct RefFetch<'w, T> {
this_run: Tick, this_run: Tick,
} }
impl<T> Clone for RefFetch<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for RefFetch<'_, T> {}
/// SAFETY: `Self` is the same as `Self::ReadOnly` /// SAFETY: `Self` is the same as `Self::ReadOnly`
unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> {
type Fetch<'w> = RefFetch<'w, T>; type Fetch<'w> = RefFetch<'w, T>;
@ -812,15 +809,6 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> {
} }
} }
unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {
RefFetch {
table_data: fetch.table_data,
sparse_set: fetch.sparse_set,
last_run: fetch.last_run,
this_run: fetch.this_run,
}
}
#[inline] #[inline]
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w>(
fetch: &mut RefFetch<'w, T>, fetch: &mut RefFetch<'w, T>,
@ -933,6 +921,13 @@ pub struct WriteFetch<'w, T> {
this_run: Tick, this_run: Tick,
} }
impl<T> Clone for WriteFetch<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for WriteFetch<'_, T> {}
/// SAFETY: access of `&T` is a subset of `&mut T` /// SAFETY: access of `&T` is a subset of `&mut T`
unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
type Fetch<'w> = WriteFetch<'w, T>; type Fetch<'w> = WriteFetch<'w, T>;
@ -975,15 +970,6 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
} }
} }
unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {
WriteFetch {
table_data: fetch.table_data,
sparse_set: fetch.sparse_set,
last_run: fetch.last_run,
this_run: fetch.this_run,
}
}
#[inline] #[inline]
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w>(
fetch: &mut WriteFetch<'w, T>, fetch: &mut WriteFetch<'w, T>,
@ -1084,6 +1070,15 @@ pub struct OptionFetch<'w, T: WorldQuery> {
matches: bool, matches: bool,
} }
impl<T: WorldQuery> Clone for OptionFetch<'_, T> {
fn clone(&self) -> Self {
Self {
fetch: self.fetch.clone(),
matches: self.matches,
}
}
}
// SAFETY: defers to soundness of `T: WorldQuery` impl // SAFETY: defers to soundness of `T: WorldQuery` impl
unsafe impl<T: WorldQuery> WorldQuery for Option<T> { unsafe impl<T: WorldQuery> WorldQuery for Option<T> {
type Fetch<'w> = OptionFetch<'w, T>; type Fetch<'w> = OptionFetch<'w, T>;
@ -1112,13 +1107,6 @@ unsafe impl<T: WorldQuery> WorldQuery for Option<T> {
} }
} }
unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {
OptionFetch {
fetch: T::clone_fetch(&fetch.fetch),
matches: fetch.matches,
}
}
#[inline] #[inline]
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w>(
fetch: &mut OptionFetch<'w, T>, fetch: &mut OptionFetch<'w, T>,
@ -1271,10 +1259,6 @@ unsafe impl<T: Component> WorldQuery for Has<T> {
false false
} }
unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {
*fetch
}
#[inline] #[inline]
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w>(
fetch: &mut Self::Fetch<'w>, fetch: &mut Self::Fetch<'w>,
@ -1351,13 +1335,6 @@ macro_rules! impl_tuple_fetch {
($($name::init_fetch(_world, $name, _last_run, _this_run),)*) ($($name::init_fetch(_world, $name, _last_run, _this_run),)*)
} }
unsafe fn clone_fetch<'w>(
fetch: &Self::Fetch<'w>,
) -> Self::Fetch<'w> {
let ($($name,)*) = &fetch;
($($name::clone_fetch($name),)*)
}
const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; const IS_DENSE: bool = true $(&& $name::IS_DENSE)*;
const IS_ARCHETYPAL: bool = true $(&& $name::IS_ARCHETYPAL)*; const IS_ARCHETYPAL: bool = true $(&& $name::IS_ARCHETYPAL)*;
@ -1461,13 +1438,6 @@ macro_rules! impl_anytuple_fetch {
($(($name::init_fetch(_world, $name, _last_run, _this_run), false),)*) ($(($name::init_fetch(_world, $name, _last_run, _this_run), false),)*)
} }
unsafe fn clone_fetch<'w>(
fetch: &Self::Fetch<'w>,
) -> Self::Fetch<'w> {
let ($($name,)*) = &fetch;
($(($name::clone_fetch(& $name.0), $name.1),)*)
}
const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; const IS_DENSE: bool = true $(&& $name::IS_DENSE)*;
const IS_ARCHETYPAL: bool = true $(&& $name::IS_ARCHETYPAL)*; const IS_ARCHETYPAL: bool = true $(&& $name::IS_ARCHETYPAL)*;
@ -1588,8 +1558,6 @@ unsafe impl<Q: WorldQuery> WorldQuery for NopWorldQuery<Q> {
) { ) {
} }
unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {}
#[inline(always)] #[inline(always)]
unsafe fn set_archetype( unsafe fn set_archetype(
_fetch: &mut (), _fetch: &mut (),
@ -1651,8 +1619,6 @@ unsafe impl<T: ?Sized> WorldQuery for PhantomData<T> {
) -> Self::Fetch<'w> { ) -> Self::Fetch<'w> {
} }
unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {}
// `PhantomData` does not match any components, so all components it matches // `PhantomData` does not match any components, so all components it matches
// are stored in a Table (vacuous truth). // are stored in a Table (vacuous truth).
const IS_DENSE: bool = true; const IS_DENSE: bool = true;

View file

@ -59,8 +59,6 @@ unsafe impl<T: Component> WorldQuery for With<T> {
) { ) {
} }
unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {}
const IS_DENSE: bool = { const IS_DENSE: bool = {
match T::Storage::STORAGE_TYPE { match T::Storage::STORAGE_TYPE {
StorageType::Table => true, StorageType::Table => true,
@ -162,8 +160,6 @@ unsafe impl<T: Component> WorldQuery for Without<T> {
) { ) {
} }
unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {}
const IS_DENSE: bool = { const IS_DENSE: bool = {
match T::Storage::STORAGE_TYPE { match T::Storage::STORAGE_TYPE {
StorageType::Table => true, StorageType::Table => true,
@ -259,6 +255,15 @@ pub struct OrFetch<'w, T: WorldQuery> {
matches: bool, matches: bool,
} }
impl<T: WorldQuery> Clone for OrFetch<'_, T> {
fn clone(&self) -> Self {
Self {
fetch: self.fetch.clone(),
matches: self.matches,
}
}
}
macro_rules! impl_query_filter_tuple { macro_rules! impl_query_filter_tuple {
($(($filter: ident, $state: ident)),*) => { ($(($filter: ident, $state: ident)),*) => {
#[allow(unused_variables)] #[allow(unused_variables)]
@ -288,18 +293,6 @@ macro_rules! impl_query_filter_tuple {
},)*) },)*)
} }
unsafe fn clone_fetch<'w>(
fetch: &Self::Fetch<'w>,
) -> Self::Fetch<'w> {
let ($($filter,)*) = &fetch;
($(
OrFetch {
fetch: $filter::clone_fetch(&$filter.fetch),
matches: $filter.matches,
},
)*)
}
#[inline] #[inline]
unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) {
let ($($filter,)*) = fetch; let ($($filter,)*) = fetch;
@ -403,10 +396,10 @@ macro_rules! impl_tick_filter {
pub struct $name<T>(PhantomData<T>); pub struct $name<T>(PhantomData<T>);
#[doc(hidden)] #[doc(hidden)]
#[derive(Clone)]
$(#[$fetch_meta])* $(#[$fetch_meta])*
pub struct $fetch_name<'w, T> { pub struct $fetch_name<'w> {
table_ticks: Option< ThinSlicePtr<'w, UnsafeCell<Tick>>>, table_ticks: Option<ThinSlicePtr<'w, UnsafeCell<Tick>>>,
marker: PhantomData<T>,
sparse_set: Option<&'w ComponentSparseSet>, sparse_set: Option<&'w ComponentSparseSet>,
last_run: Tick, last_run: Tick,
this_run: Tick, this_run: Tick,
@ -414,7 +407,7 @@ macro_rules! impl_tick_filter {
// SAFETY: `Self::ReadOnly` is the same as `Self` // SAFETY: `Self::ReadOnly` is the same as `Self`
unsafe impl<T: Component> WorldQuery for $name<T> { unsafe impl<T: Component> WorldQuery for $name<T> {
type Fetch<'w> = $fetch_name<'w, T>; type Fetch<'w> = $fetch_name<'w>;
type Item<'w> = bool; type Item<'w> = bool;
type ReadOnly = Self; type ReadOnly = Self;
type State = ComponentId; type State = ComponentId;
@ -440,24 +433,11 @@ macro_rules! impl_tick_filter {
.get(id) .get(id)
.debug_checked_unwrap() .debug_checked_unwrap()
}), }),
marker: PhantomData,
last_run, last_run,
this_run, this_run,
} }
} }
unsafe fn clone_fetch<'w>(
fetch: &Self::Fetch<'w>,
) -> Self::Fetch<'w> {
$fetch_name {
table_ticks: fetch.table_ticks,
sparse_set: fetch.sparse_set,
last_run: fetch.last_run,
this_run: fetch.this_run,
marker: PhantomData,
}
}
const IS_DENSE: bool = { const IS_DENSE: bool = {
match T::Storage::STORAGE_TYPE { match T::Storage::STORAGE_TYPE {
StorageType::Table => true, StorageType::Table => true,

View file

@ -6,7 +6,7 @@ use crate::{
storage::{TableId, TableRow, Tables}, storage::{TableId, TableRow, Tables},
world::unsafe_world_cell::UnsafeWorldCell, world::unsafe_world_cell::UnsafeWorldCell,
}; };
use std::{borrow::Borrow, iter::FusedIterator, marker::PhantomData, mem::MaybeUninit}; use std::{borrow::Borrow, iter::FusedIterator, mem::MaybeUninit};
use super::ReadOnlyWorldQuery; use super::ReadOnlyWorldQuery;
@ -163,7 +163,9 @@ where
// SAFETY: set_archetype was called prior. // SAFETY: set_archetype was called prior.
// `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d // `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d
if F::filter_fetch(&mut self.filter, entity, location.table_row) { if F::filter_fetch(&mut self.filter, entity, location.table_row) {
// SAFETY: set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // SAFETY:
// - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype
// - fetch is only called once for each entity.
return Some(Q::fetch(&mut self.fetch, entity, location.table_row)); return Some(Q::fetch(&mut self.fetch, entity, location.table_row));
} }
} }
@ -344,7 +346,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery, const K: usize>
match self.cursors[i].next(self.tables, self.archetypes, self.query_state) { match self.cursors[i].next(self.tables, self.archetypes, self.query_state) {
Some(_) => { Some(_) => {
for j in (i + 1)..K { for j in (i + 1)..K {
self.cursors[j] = self.cursors[j - 1].clone_cursor(); self.cursors[j] = self.cursors[j - 1].clone();
match self.cursors[j].next(self.tables, self.archetypes, self.query_state) { match self.cursors[j].next(self.tables, self.archetypes, self.query_state) {
Some(_) => {} Some(_) => {}
None if i > 0 => continue 'outer, None if i > 0 => continue 'outer,
@ -453,28 +455,19 @@ struct QueryIterationCursor<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> {
current_len: usize, current_len: usize,
// either table row or archetype index, depending on whether both `Q`'s and `F`'s fetches are dense // either table row or archetype index, depending on whether both `Q`'s and `F`'s fetches are dense
current_row: usize, current_row: usize,
phantom: PhantomData<Q>,
} }
impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryIterationCursor<'w, 's, Q, F> { impl<Q: WorldQuery, F: ReadOnlyWorldQuery> Clone for QueryIterationCursor<'_, '_, Q, F> {
/// This function is safe to call if `(Q, F): ReadOnlyWorldQuery` holds. fn clone(&self) -> Self {
///
/// # Safety
/// While calling this method on its own cannot cause UB it is marked `unsafe` as the caller must ensure
/// that the returned value is not used in any way that would cause two `QueryItem<Q>` for the same
/// `archetype_row` or `table_row` to be alive at the same time.
unsafe fn clone_cursor(&self) -> Self {
Self { Self {
table_id_iter: self.table_id_iter.clone(), table_id_iter: self.table_id_iter.clone(),
archetype_id_iter: self.archetype_id_iter.clone(), archetype_id_iter: self.archetype_id_iter.clone(),
table_entities: self.table_entities, table_entities: self.table_entities,
archetype_entities: self.archetype_entities, archetype_entities: self.archetype_entities,
// SAFETY: upheld by caller invariants fetch: self.fetch.clone(),
fetch: Q::clone_fetch(&self.fetch), filter: self.filter.clone(),
filter: F::clone_fetch(&self.filter),
current_len: self.current_len, current_len: self.current_len,
current_row: self.current_row, current_row: self.current_row,
phantom: PhantomData,
} }
} }
} }
@ -515,7 +508,6 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryIterationCursor<'w, 's,
archetype_id_iter: query_state.matched_archetype_ids.iter(), archetype_id_iter: query_state.matched_archetype_ids.iter(),
current_len: 0, current_len: 0,
current_row: 0, current_row: 0,
phantom: PhantomData,
} }
} }
@ -593,8 +585,11 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryIterationCursor<'w, 's,
continue; continue;
} }
// SAFETY: set_table was called prior. // SAFETY:
// `current_row` is a table row in range of the current table, because if it was not, then the if above would have been executed. // - set_table was called prior.
// - `current_row` must be a table row in range of the current table,
// because if it was not, then the if above would have been executed.
// - fetch is only called once for each `entity`.
let item = Q::fetch(&mut self.fetch, *entity, row); let item = Q::fetch(&mut self.fetch, *entity, row);
self.current_row += 1; self.current_row += 1;
@ -633,8 +628,11 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryIterationCursor<'w, 's,
continue; continue;
} }
// SAFETY: set_archetype was called prior, `current_row` is an archetype index in range of the current archetype // SAFETY:
// `current_row` is an archetype index row in range of the current archetype, because if it was not, then the if above would have been executed. // - set_archetype was called prior.
// - `current_row` must be an archetype index row in range of the current archetype,
// because if it was not, then the if above would have been executed.
// - fetch is only called once for each `archetype_entity`.
let item = Q::fetch( let item = Q::fetch(
&mut self.fetch, &mut self.fetch,
archetype_entity.entity(), archetype_entity.entity(),

View file

@ -778,7 +778,7 @@ mod tests {
world.spawn((A(1), B(1))); world.spawn((A(1), B(1)));
fn propagate_system(mut query: Query<(&A, &mut B), Changed<A>>) { fn propagate_system(mut query: Query<(&A, &mut B), Changed<A>>) {
query.par_iter_mut().for_each_mut(|(a, mut b)| { query.par_iter_mut().for_each(|(a, mut b)| {
b.0 = a.0; b.0 = a.0;
}); });
} }

View file

@ -1,7 +1,7 @@
use crate::{component::Tick, world::unsafe_world_cell::UnsafeWorldCell}; use crate::{component::Tick, world::unsafe_world_cell::UnsafeWorldCell};
use std::ops::Range; use std::ops::Range;
use super::{QueryItem, QueryState, ROQueryItem, ReadOnlyWorldQuery, WorldQuery}; use super::{QueryItem, QueryState, ReadOnlyWorldQuery, WorldQuery};
/// Dictates how a parallel query chunks up large tables/archetypes /// Dictates how a parallel query chunks up large tables/archetypes
/// during iteration. /// during iteration.
@ -90,26 +90,6 @@ pub struct QueryParIter<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> {
pub(crate) batching_strategy: BatchingStrategy, pub(crate) batching_strategy: BatchingStrategy,
} }
impl<'w, 's, Q: ReadOnlyWorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> {
/// Runs `func` on each query result in parallel.
///
/// This can only be called for read-only queries, see [`Self::for_each_mut`] for
/// write-queries.
///
/// # Panics
/// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being
/// initialized and run from the ECS scheduler, this should never panic.
///
/// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool
#[inline]
pub fn for_each<FN: Fn(ROQueryItem<'w, Q>) + Send + Sync + Clone>(&self, func: FN) {
// SAFETY: query is read only
unsafe {
self.for_each_unchecked(func);
}
}
}
impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> { impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> {
/// Changes the batching strategy used when iterating. /// Changes the batching strategy used when iterating.
/// ///
@ -123,50 +103,47 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> {
/// Runs `func` on each query result in parallel. /// Runs `func` on each query result in parallel.
/// ///
/// # Panics /// # Panics
/// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being /// If the [`ComputeTaskPool`] is not initialized. If using this from a query that is being
/// initialized and run from the ECS scheduler, this should never panic. /// initialized and run from the ECS scheduler, this should never panic.
/// ///
/// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool
#[inline] #[inline]
pub fn for_each_mut<FN: Fn(QueryItem<'w, Q>) + Send + Sync + Clone>(&mut self, func: FN) { pub fn for_each<FN: Fn(QueryItem<'w, Q>) + Send + Sync + Clone>(self, func: FN) {
// SAFETY: query has unique world access
unsafe {
self.for_each_unchecked(func);
}
}
/// Runs `func` on each query result in parallel.
///
/// # Panics
/// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being
/// initialized and run from the ECS scheduler, this should never panic.
///
/// # Safety
///
/// This does not check for mutable query correctness. To be safe, make sure mutable queries
/// have unique access to the components they query.
///
/// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool
#[inline]
unsafe fn for_each_unchecked<FN: Fn(QueryItem<'w, Q>) + Send + Sync + Clone>(&self, func: FN) {
#[cfg(any(target = "wasm32", not(feature = "multi-threaded")))] #[cfg(any(target = "wasm32", not(feature = "multi-threaded")))]
{ {
self.state // SAFETY:
.for_each_unchecked_manual(self.world, func, self.last_run, self.this_run); // This method can only be called once per instance of QueryParIter,
} // which ensures that mutable queries cannot be executed multiple times at once.
#[cfg(all(not(target = "wasm32"), feature = "multi-threaded"))] // Mutable instances of QueryParIter can only be created via an exclusive borrow of a
{ // Query or a World, which ensures that multiple aliasing QueryParIters cannot exist
let thread_count = bevy_tasks::ComputeTaskPool::get().thread_num(); // at the same time.
if thread_count <= 1 { unsafe {
self.state.for_each_unchecked_manual( self.state.for_each_unchecked_manual(
self.world, self.world,
func, func,
self.last_run, self.last_run,
self.this_run, self.this_run,
); );
}
}
#[cfg(all(not(target = "wasm32"), feature = "multi-threaded"))]
{
let thread_count = bevy_tasks::ComputeTaskPool::get().thread_num();
if thread_count <= 1 {
// SAFETY: See the safety comment above.
unsafe {
self.state.for_each_unchecked_manual(
self.world,
func,
self.last_run,
self.this_run,
);
}
} else { } else {
// Need a batch size of at least 1. // Need a batch size of at least 1.
let batch_size = self.get_batch_size(thread_count).max(1); let batch_size = self.get_batch_size(thread_count).max(1);
// SAFETY: See the safety comment above.
unsafe {
self.state.par_for_each_unchecked_manual( self.state.par_for_each_unchecked_manual(
self.world, self.world,
batch_size, batch_size,
@ -177,6 +154,20 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> {
} }
} }
} }
}
/// Runs `func` on each query result in parallel.
///
/// # Panics
/// If the [`ComputeTaskPool`] is not initialized. If using this from a query that is being
/// initialized and run from the ECS scheduler, this should never panic.
///
/// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool
#[inline]
#[deprecated = "use `.for_each(...)` instead."]
pub fn for_each_mut<FN: Fn(QueryItem<'w, Q>) + Send + Sync + Clone>(self, func: FN) {
self.for_each(func);
}
#[cfg(all(not(target = "wasm32"), feature = "multi-threaded"))] #[cfg(all(not(target = "wasm32"), feature = "multi-threaded"))]
fn get_batch_size(&self, thread_count: usize) -> usize { fn get_batch_size(&self, thread_count: usize) -> usize {

View file

@ -235,13 +235,13 @@ use std::{any::TypeId, borrow::Borrow, fmt::Debug};
/// ///
/// |Query methods|Effect| /// |Query methods|Effect|
/// |:---:|---| /// |:---:|---|
/// |[`iter`]\([`_mut`][`iter_mut`])|Returns an iterator over all query items.| /// |[`iter`]\[[`_mut`][`iter_mut`]]|Returns an iterator over all query items.|
/// |[`for_each`]\([`_mut`][`for_each_mut`]),<br>[`par_iter`]\([`_mut`][`par_iter_mut`])|Runs a specified function for each query item.| /// |[`for_each`]\[[`_mut`][`for_each_mut`]],<br>[`par_iter`]\[[`_mut`][`par_iter_mut`]]|Runs a specified function for each query item.|
/// |[`iter_many`]\([`_mut`][`iter_many_mut`])|Iterates or runs a specified function over query items generated by a list of entities.| /// |[`iter_many`]\[[`_mut`][`iter_many_mut`]]|Iterates or runs a specified function over query items generated by a list of entities.|
/// |[`iter_combinations`]\([`_mut`][`iter_combinations_mut`])|Returns an iterator over all combinations of a specified number of query items.| /// |[`iter_combinations`]\[[`_mut`][`iter_combinations_mut`]]|Returns an iterator over all combinations of a specified number of query items.|
/// |[`get`]\([`_mut`][`get_mut`])|Returns the query item for the specified entity.| /// |[`get`]\[[`_mut`][`get_mut`]]|Returns the query item for the specified entity.|
/// |[`many`]\([`_mut`][`many_mut`]),<br>[`get_many`]\([`_mut`][`get_many_mut`])|Returns the query items for the specified entities.| /// |[`many`]\[[`_mut`][`many_mut`]],<br>[`get_many`]\[[`_mut`][`get_many_mut`]]|Returns the query items for the specified entities.|
/// |[`single`]\([`_mut`][`single_mut`]),<br>[`get_single`]\([`_mut`][`get_single_mut`])|Returns the query item while verifying that there aren't others.| /// |[`single`]\[[`_mut`][`single_mut`]],<br>[`get_single`]\[[`_mut`][`get_single_mut`]]|Returns the query item while verifying that there aren't others.|
/// ///
/// There are two methods for each type of query operation: immutable and mutable (ending with `_mut`). /// There are two methods for each type of query operation: immutable and mutable (ending with `_mut`).
/// When using immutable methods, the query items returned are of type [`ROQueryItem`], a read-only version of the query item. /// When using immutable methods, the query items returned are of type [`ROQueryItem`], a read-only version of the query item.
@ -271,14 +271,14 @@ use std::{any::TypeId, borrow::Borrow, fmt::Debug};
/// ///
/// |Query operation|Computational complexity| /// |Query operation|Computational complexity|
/// |:---:|:---:| /// |:---:|:---:|
/// |[`iter`]\([`_mut`][`iter_mut`])|O(n)| /// |[`iter`]\[[`_mut`][`iter_mut`]]|O(n)|
/// |[`for_each`]\([`_mut`][`for_each_mut`]),<br>[`par_iter`]\([`_mut`][`par_iter_mut`])|O(n)| /// |[`for_each`]\[[`_mut`][`for_each_mut`]],<br>[`par_iter`]\[[`_mut`][`par_iter_mut`]]|O(n)|
/// |[`iter_many`]\([`_mut`][`iter_many_mut`])|O(k)| /// |[`iter_many`]\[[`_mut`][`iter_many_mut`]]|O(k)|
/// |[`iter_combinations`]\([`_mut`][`iter_combinations_mut`])|O(<sub>n</sub>C<sub>r</sub>)| /// |[`iter_combinations`]\[[`_mut`][`iter_combinations_mut`]]|O(<sub>n</sub>C<sub>r</sub>)|
/// |[`get`]\([`_mut`][`get_mut`])|O(1)| /// |[`get`]\[[`_mut`][`get_mut`]]|O(1)|
/// |([`get_`][`get_many`])[`many`]|O(k)| /// |([`get_`][`get_many`])[`many`]|O(k)|
/// |([`get_`][`get_many_mut`])[`many_mut`]|O(k<sup>2</sup>)| /// |([`get_`][`get_many_mut`])[`many_mut`]|O(k<sup>2</sup>)|
/// |[`single`]\([`_mut`][`single_mut`]),<br>[`get_single`]\([`_mut`][`get_single_mut`])|O(a)| /// |[`single`]\[[`_mut`][`single_mut`]],<br>[`get_single`]\[[`_mut`][`get_single_mut`]]|O(a)|
/// |Archetype based filtering ([`With`], [`Without`], [`Or`])|O(a)| /// |Archetype based filtering ([`With`], [`Without`], [`Or`])|O(a)|
/// |Change detection filtering ([`Added`], [`Changed`])|O(a + n)| /// |Change detection filtering ([`Added`], [`Changed`])|O(a + n)|
/// ///

View file

@ -18,7 +18,7 @@
use std::mem; use std::mem;
use bevy_app::{Last, Plugin, Update}; use bevy_app::{Last, Plugin, PostUpdate};
use bevy_asset::{load_internal_asset, AddAsset, Assets, Handle, HandleUntyped}; use bevy_asset::{load_internal_asset, AddAsset, Assets, Handle, HandleUntyped};
use bevy_core::cast_slice; use bevy_core::cast_slice;
use bevy_ecs::{ use bevy_ecs::{
@ -50,7 +50,10 @@ use bevy_render::{
view::RenderLayers, view::RenderLayers,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_transform::components::{GlobalTransform, Transform}; use bevy_transform::{
components::{GlobalTransform, Transform},
TransformSystem,
};
pub mod gizmos; pub mod gizmos;
@ -85,11 +88,12 @@ impl Plugin for GizmoPlugin {
.init_resource::<GizmoStorage>() .init_resource::<GizmoStorage>()
.add_systems(Last, update_gizmo_meshes) .add_systems(Last, update_gizmo_meshes)
.add_systems( .add_systems(
Update, PostUpdate,
( (
draw_aabbs, draw_aabbs,
draw_all_aabbs.run_if(|config: Res<GizmoConfig>| config.aabb.draw_all), draw_all_aabbs.run_if(|config: Res<GizmoConfig>| config.aabb.draw_all),
), )
.after(TransformSystem::TransformPropagate),
); );
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; };
@ -233,13 +237,17 @@ fn draw_all_aabbs(
} }
fn color_from_entity(entity: Entity) -> Color { fn color_from_entity(entity: Entity) -> Color {
use bevy_utils::RandomState; let index = entity.index();
const U64_TO_DEGREES: f32 = 360.0 / u64::MAX as f32;
const STATE: RandomState = // from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
RandomState::with_seeds(5952553601252303067, 16866614500153072625, 0, 0); //
// See https://en.wikipedia.org/wiki/Low-discrepancy_sequence
// Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range,
// so that the closer the numbers are, the larger the difference of their image.
const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
const RATIO_360: f32 = 360.0 / u32::MAX as f32;
let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360;
let hash = STATE.hash_one(entity);
let hue = hash as f32 * U64_TO_DEGREES;
Color::hsl(hue, 1., 0.5) Color::hsl(hue, 1., 0.5)
} }

View file

@ -40,21 +40,24 @@ impl GltfPlugin {
impl Plugin for GltfPlugin { impl Plugin for GltfPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
let supported_compressed_formats = match app.world.get_resource::<RenderDevice>() { app.register_type::<GltfExtras>()
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::all(),
};
app.add_asset_loader::<GltfLoader>(GltfLoader {
supported_compressed_formats,
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
})
.register_type::<GltfExtras>()
.add_asset::<Gltf>() .add_asset::<Gltf>()
.add_asset::<GltfNode>() .add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>() .add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>(); .add_asset::<GltfMesh>();
} }
fn finish(&self, app: &mut App) {
let supported_compressed_formats = match app.world.get_resource::<RenderDevice>() {
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::NONE,
};
app.add_asset_loader::<GltfLoader>(GltfLoader {
supported_compressed_formats,
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
});
}
} }
/// Representation of a loaded glTF file. /// Representation of a loaded glTF file.

View file

@ -28,10 +28,20 @@ impl Default for BevyManifest {
.map(PathBuf::from) .map(PathBuf::from)
.map(|mut path| { .map(|mut path| {
path.push("Cargo.toml"); path.push("Cargo.toml");
let manifest = std::fs::read_to_string(path).unwrap(); if !path.exists() {
manifest.parse::<Document>().unwrap() panic!(
"No Cargo manifest found for crate. Expected: {}",
path.display()
);
}
let manifest = std::fs::read_to_string(path.clone()).unwrap_or_else(|_| {
panic!("Unable to read cargo manifest: {}", path.display())
});
manifest.parse::<Document>().unwrap_or_else(|_| {
panic!("Failed to parse cargo manifest: {}", path.display())
}) })
.unwrap(), })
.expect("CARGO_MANIFEST_DIR is not defined."),
} }
} }
} }

View file

@ -9,7 +9,7 @@ use crate::{IVec2, Rect, URect};
/// methods instead, which will ensure this invariant is met, unless you already have /// methods instead, which will ensure this invariant is met, unless you already have
/// the minimum and maximum corners. /// the minimum and maximum corners.
#[repr(C)] #[repr(C)]
#[derive(Default, Clone, Copy, Debug, PartialEq)] #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct IRect { pub struct IRect {
/// The minimum corner point of the rect. /// The minimum corner point of the rect.

View file

@ -9,7 +9,7 @@ use crate::{IRect, Rect, UVec2};
/// methods instead, which will ensure this invariant is met, unless you already have /// methods instead, which will ensure this invariant is met, unless you already have
/// the minimum and maximum corners. /// the minimum and maximum corners.
#[repr(C)] #[repr(C)]
#[derive(Default, Clone, Copy, Debug, PartialEq)] #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct URect { pub struct URect {
/// The minimum corner point of the rect. /// The minimum corner point of the rect.

View file

@ -166,6 +166,7 @@ impl Plugin for PbrPlugin {
); );
app.register_asset_reflect::<StandardMaterial>() app.register_asset_reflect::<StandardMaterial>()
.register_type::<AlphaMode>()
.register_type::<AmbientLight>() .register_type::<AmbientLight>()
.register_type::<Cascade>() .register_type::<Cascade>()
.register_type::<CascadeShadowConfig>() .register_type::<CascadeShadowConfig>()

View file

@ -1,9 +1,18 @@
use crate as bevy_reflect; use crate as bevy_reflect;
use crate::prelude::ReflectDefault; use crate::prelude::ReflectDefault;
use crate::{ReflectDeserialize, ReflectSerialize}; use crate::{ReflectDeserialize, ReflectSerialize};
use bevy_math::{Rect, Vec2}; use bevy_math::{IRect, IVec2, Rect, URect, UVec2, Vec2};
use bevy_reflect_derive::impl_reflect_struct; use bevy_reflect_derive::impl_reflect_struct;
impl_reflect_struct!(
#[reflect(Debug, PartialEq, Hash, Serialize, Deserialize, Default)]
#[type_path = "bevy_math"]
struct IRect {
min: IVec2,
max: IVec2,
}
);
impl_reflect_struct!( impl_reflect_struct!(
#[reflect(Debug, PartialEq, Serialize, Deserialize, Default)] #[reflect(Debug, PartialEq, Serialize, Deserialize, Default)]
#[type_path = "bevy_math"] #[type_path = "bevy_math"]
@ -12,3 +21,12 @@ impl_reflect_struct!(
max: Vec2, max: Vec2,
} }
); );
impl_reflect_struct!(
#[reflect(Debug, PartialEq, Hash, Serialize, Deserialize, Default)]
#[type_path = "bevy_math"]
struct URect {
min: UVec2,
max: UVec2,
}
);

View file

@ -58,7 +58,6 @@ image = { version = "0.24", default-features = false }
# misc # misc
wgpu = { version = "0.16.0", features=["naga"] } wgpu = { version = "0.16.0", features=["naga"] }
wgpu-hal = "0.16.0"
codespan-reporting = "0.11.0" codespan-reporting = "0.11.0"
naga = { version = "0.12.0", features = ["wgsl-in"] } naga = { version = "0.12.0", features = ["wgsl-in"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }

View file

@ -0,0 +1,55 @@
use crate::{
render_resource::{GpuArrayBuffer, GpuArrayBufferable},
renderer::{RenderDevice, RenderQueue},
Render, RenderApp, RenderSet,
};
use bevy_app::{App, Plugin};
use bevy_ecs::{
prelude::{Component, Entity},
schedule::IntoSystemConfigs,
system::{Commands, Query, Res, ResMut},
};
use std::marker::PhantomData;
/// This plugin prepares the components of the corresponding type for the GPU
/// by storing them in a [`GpuArrayBuffer`].
pub struct GpuComponentArrayBufferPlugin<C: Component + GpuArrayBufferable>(PhantomData<C>);
impl<C: Component + GpuArrayBufferable> Plugin for GpuComponentArrayBufferPlugin<C> {
fn build(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.insert_resource(GpuArrayBuffer::<C>::new(
render_app.world.resource::<RenderDevice>(),
))
.add_systems(
Render,
prepare_gpu_component_array_buffers::<C>.in_set(RenderSet::Prepare),
);
}
}
}
impl<C: Component + GpuArrayBufferable> Default for GpuComponentArrayBufferPlugin<C> {
fn default() -> Self {
Self(PhantomData::<C>)
}
}
fn prepare_gpu_component_array_buffers<C: Component + GpuArrayBufferable>(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut gpu_array_buffer: ResMut<GpuArrayBuffer<C>>,
components: Query<(Entity, &C)>,
) {
gpu_array_buffer.clear();
let entities = components
.iter()
.map(|(entity, component)| (entity, gpu_array_buffer.push(component.clone())))
.collect::<Vec<_>>();
commands.insert_or_spawn_batch(entities);
gpu_array_buffer.write_buffer(&render_device, &render_queue);
}

View file

@ -11,6 +11,7 @@ pub mod extract_component;
mod extract_param; mod extract_param;
pub mod extract_resource; pub mod extract_resource;
pub mod globals; pub mod globals;
pub mod gpu_component_array_buffer;
pub mod mesh; pub mod mesh;
pub mod pipelined_rendering; pub mod pipelined_rendering;
pub mod primitives; pub mod primitives;

View file

@ -103,21 +103,29 @@ impl Plugin for PipelinedRenderingPlugin {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _span = bevy_utils::tracing::info_span!("render thread").entered(); let _span = bevy_utils::tracing::info_span!("render thread").entered();
let compute_task_pool = ComputeTaskPool::get();
loop { loop {
// run a scope here to allow main world to use this thread while it's waiting for the render app // run a scope here to allow main world to use this thread while it's waiting for the render app
let mut render_app = ComputeTaskPool::get() let sent_app = compute_task_pool
.scope(|s| { .scope(|s| {
s.spawn(async { app_to_render_receiver.recv().await.unwrap() }); s.spawn(async { app_to_render_receiver.recv().await });
}) })
.pop() .pop();
.unwrap(); let Some(Ok(mut render_app)) = sent_app else { break };
{
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _sub_app_span = let _sub_app_span =
bevy_utils::tracing::info_span!("sub app", name = ?RenderApp).entered(); bevy_utils::tracing::info_span!("sub app", name = ?RenderApp).entered();
render_app.run(); render_app.run();
render_to_app_sender.send_blocking(render_app).unwrap();
} }
if render_to_app_sender.send_blocking(render_app).is_err() {
break;
}
}
bevy_utils::tracing::debug!("exiting pipelined rendering thread");
}); });
} }
} }

View file

@ -0,0 +1,152 @@
use super::{GpuArrayBufferIndex, GpuArrayBufferable};
use crate::{
render_resource::DynamicUniformBuffer,
renderer::{RenderDevice, RenderQueue},
};
use encase::{
private::{ArrayMetadata, BufferMut, Metadata, RuntimeSizedArray, WriteInto, Writer},
ShaderType,
};
use std::{marker::PhantomData, num::NonZeroU64};
use wgpu::{BindingResource, Limits};
// 1MB else we will make really large arrays on macOS which reports very large
// `max_uniform_buffer_binding_size`. On macOS this ends up being the minimum
// size of the uniform buffer as well as the size of each chunk of data at a
// dynamic offset.
#[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
const MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE: u32 = 1 << 20;
// WebGL2 quirk: using uniform buffers larger than 4KB will cause extremely
// long shader compilation times, so the limit needs to be lower on WebGL2.
// This is due to older shader compilers/GPUs that don't support dynamically
// indexing uniform buffers, and instead emulate it with large switch statements
// over buffer indices that take a long time to compile.
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
const MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE: u32 = 1 << 12;
/// Similar to [`DynamicUniformBuffer`], except every N elements (depending on size)
/// are grouped into a batch as an `array<T, N>` in WGSL.
///
/// This reduces the number of rebindings required due to having to pass dynamic
/// offsets to bind group commands, and if indices into the array can be passed
/// in via other means, it enables batching of draw commands.
pub struct BatchedUniformBuffer<T: GpuArrayBufferable> {
// Batches of fixed-size arrays of T are written to this buffer so that
// each batch in a fixed-size array can be bound at a dynamic offset.
uniforms: DynamicUniformBuffer<MaxCapacityArray<Vec<T>>>,
// A batch of T are gathered into this `MaxCapacityArray` until it is full,
// then it is written into the `DynamicUniformBuffer`, cleared, and new T
// are gathered here, and so on for each batch.
temp: MaxCapacityArray<Vec<T>>,
current_offset: u32,
dynamic_offset_alignment: u32,
}
impl<T: GpuArrayBufferable> BatchedUniformBuffer<T> {
pub fn batch_size(limits: &Limits) -> usize {
(limits
.max_uniform_buffer_binding_size
.min(MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE) as u64
/ T::min_size().get()) as usize
}
pub fn new(limits: &Limits) -> Self {
let capacity = Self::batch_size(limits);
let alignment = limits.min_uniform_buffer_offset_alignment;
Self {
uniforms: DynamicUniformBuffer::new_with_alignment(alignment as u64),
temp: MaxCapacityArray(Vec::with_capacity(capacity), capacity),
current_offset: 0,
dynamic_offset_alignment: alignment,
}
}
#[inline]
pub fn size(&self) -> NonZeroU64 {
self.temp.size()
}
pub fn clear(&mut self) {
self.uniforms.clear();
self.current_offset = 0;
self.temp.0.clear();
}
pub fn push(&mut self, component: T) -> GpuArrayBufferIndex<T> {
let result = GpuArrayBufferIndex {
index: self.temp.0.len() as u32,
dynamic_offset: Some(self.current_offset),
element_type: PhantomData,
};
self.temp.0.push(component);
if self.temp.0.len() == self.temp.1 {
self.flush();
}
result
}
pub fn flush(&mut self) {
self.uniforms.push(self.temp.clone());
self.current_offset +=
align_to_next(self.temp.size().get(), self.dynamic_offset_alignment as u64) as u32;
self.temp.0.clear();
}
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
if !self.temp.0.is_empty() {
self.flush();
}
self.uniforms.write_buffer(device, queue);
}
#[inline]
pub fn binding(&self) -> Option<BindingResource> {
let mut binding = self.uniforms.binding();
if let Some(BindingResource::Buffer(binding)) = &mut binding {
// MaxCapacityArray is runtime-sized so can't use T::min_size()
binding.size = Some(self.size());
}
binding
}
}
#[inline]
fn align_to_next(value: u64, alignment: u64) -> u64 {
debug_assert!(alignment & (alignment - 1) == 0);
((value - 1) | (alignment - 1)) + 1
}
// ----------------------------------------------------------------------------
// MaxCapacityArray was implemented by Teodor Tanasoaia for encase. It was
// copied here as it was not yet included in an encase release and it is
// unclear if it is the correct long-term solution for encase.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct MaxCapacityArray<T>(T, usize);
impl<T> ShaderType for MaxCapacityArray<T>
where
T: ShaderType<ExtraMetadata = ArrayMetadata>,
{
type ExtraMetadata = ArrayMetadata;
const METADATA: Metadata<Self::ExtraMetadata> = T::METADATA;
fn size(&self) -> ::core::num::NonZeroU64 {
Self::METADATA.stride().mul(self.1.max(1) as u64).0
}
}
impl<T> WriteInto for MaxCapacityArray<T>
where
T: WriteInto + RuntimeSizedArray,
{
fn write_into<B: BufferMut>(&self, writer: &mut Writer<B>) {
debug_assert!(self.0.len() <= self.1);
self.0.write_into(writer);
}
}

View file

@ -21,9 +21,11 @@ use wgpu::BufferUsages;
/// from system RAM to VRAM. /// from system RAM to VRAM.
/// ///
/// Other options for storing GPU-accessible data are: /// Other options for storing GPU-accessible data are:
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) /// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) /// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) /// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
/// * [`BufferVec`](crate::render_resource::BufferVec) /// * [`BufferVec`](crate::render_resource::BufferVec)
/// * [`Texture`](crate::render_resource::Texture) /// * [`Texture`](crate::render_resource::Texture)
pub struct BufferVec<T: Pod> { pub struct BufferVec<T: Pod> {

View file

@ -0,0 +1,129 @@
use super::StorageBuffer;
use crate::{
render_resource::batched_uniform_buffer::BatchedUniformBuffer,
renderer::{RenderDevice, RenderQueue},
};
use bevy_ecs::{prelude::Component, system::Resource};
use encase::{private::WriteInto, ShaderSize, ShaderType};
use std::{marker::PhantomData, mem};
use wgpu::{BindGroupLayoutEntry, BindingResource, BindingType, BufferBindingType, ShaderStages};
/// Trait for types able to go in a [`GpuArrayBuffer`].
pub trait GpuArrayBufferable: ShaderType + ShaderSize + WriteInto + Clone {}
impl<T: ShaderType + ShaderSize + WriteInto + Clone> GpuArrayBufferable for T {}
/// Stores an array of elements to be transferred to the GPU and made accessible to shaders as a read-only array.
///
/// On platforms that support storage buffers, this is equivalent to [`StorageBuffer<Vec<T>>`].
/// Otherwise, this falls back to a dynamic offset uniform buffer with the largest
/// array of T that fits within a uniform buffer binding (within reasonable limits).
///
/// Other options for storing GPU-accessible data are:
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
/// * [`BufferVec`](crate::render_resource::BufferVec)
/// * [`Texture`](crate::render_resource::Texture)
#[derive(Resource)]
pub enum GpuArrayBuffer<T: GpuArrayBufferable> {
Uniform(BatchedUniformBuffer<T>),
Storage((StorageBuffer<Vec<T>>, Vec<T>)),
}
impl<T: GpuArrayBufferable> GpuArrayBuffer<T> {
pub fn new(device: &RenderDevice) -> Self {
let limits = device.limits();
if limits.max_storage_buffers_per_shader_stage == 0 {
GpuArrayBuffer::Uniform(BatchedUniformBuffer::new(&limits))
} else {
GpuArrayBuffer::Storage((StorageBuffer::default(), Vec::new()))
}
}
pub fn clear(&mut self) {
match self {
GpuArrayBuffer::Uniform(buffer) => buffer.clear(),
GpuArrayBuffer::Storage((_, buffer)) => buffer.clear(),
}
}
pub fn push(&mut self, value: T) -> GpuArrayBufferIndex<T> {
match self {
GpuArrayBuffer::Uniform(buffer) => buffer.push(value),
GpuArrayBuffer::Storage((_, buffer)) => {
let index = buffer.len() as u32;
buffer.push(value);
GpuArrayBufferIndex {
index,
dynamic_offset: None,
element_type: PhantomData,
}
}
}
}
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
match self {
GpuArrayBuffer::Uniform(buffer) => buffer.write_buffer(device, queue),
GpuArrayBuffer::Storage((buffer, vec)) => {
buffer.set(mem::take(vec));
buffer.write_buffer(device, queue);
}
}
}
pub fn binding_layout(
binding: u32,
visibility: ShaderStages,
device: &RenderDevice,
) -> BindGroupLayoutEntry {
BindGroupLayoutEntry {
binding,
visibility,
ty: if device.limits().max_storage_buffers_per_shader_stage == 0 {
BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
// BatchedUniformBuffer uses a MaxCapacityArray that is runtime-sized, so we use
// None here and let wgpu figure out the size.
min_binding_size: None,
}
} else {
BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: Some(T::min_size()),
}
},
count: None,
}
}
pub fn binding(&self) -> Option<BindingResource> {
match self {
GpuArrayBuffer::Uniform(buffer) => buffer.binding(),
GpuArrayBuffer::Storage((buffer, _)) => buffer.binding(),
}
}
pub fn batch_size(device: &RenderDevice) -> Option<u32> {
let limits = device.limits();
if limits.max_storage_buffers_per_shader_stage == 0 {
Some(BatchedUniformBuffer::<T>::batch_size(&limits) as u32)
} else {
None
}
}
}
/// An index into a [`GpuArrayBuffer`] for a given element.
#[derive(Component)]
pub struct GpuArrayBufferIndex<T: GpuArrayBufferable> {
/// The index to use in a shader into the array.
pub index: u32,
/// The dynamic offset to use when setting the bind group in a pass.
/// Only used on platforms that don't support storage buffers.
pub dynamic_offset: Option<u32>,
pub element_type: PhantomData<T>,
}

View file

@ -1,7 +1,9 @@
mod batched_uniform_buffer;
mod bind_group; mod bind_group;
mod bind_group_layout; mod bind_group_layout;
mod buffer; mod buffer;
mod buffer_vec; mod buffer_vec;
mod gpu_array_buffer;
mod pipeline; mod pipeline;
mod pipeline_cache; mod pipeline_cache;
mod pipeline_specializer; mod pipeline_specializer;
@ -15,6 +17,7 @@ pub use bind_group::*;
pub use bind_group_layout::*; pub use bind_group_layout::*;
pub use buffer::*; pub use buffer::*;
pub use buffer_vec::*; pub use buffer_vec::*;
pub use gpu_array_buffer::*;
pub use pipeline::*; pub use pipeline::*;
pub use pipeline_cache::*; pub use pipeline_cache::*;
pub use pipeline_specializer::*; pub use pipeline_specializer::*;

View file

@ -25,6 +25,7 @@ use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferUsa
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) /// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) /// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) /// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
/// * [`BufferVec`](crate::render_resource::BufferVec) /// * [`BufferVec`](crate::render_resource::BufferVec)
/// * [`Texture`](crate::render_resource::Texture) /// * [`Texture`](crate::render_resource::Texture)
/// ///
@ -154,6 +155,7 @@ impl<T: ShaderType + WriteInto> StorageBuffer<T> {
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) /// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) /// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) /// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
/// * [`BufferVec`](crate::render_resource::BufferVec) /// * [`BufferVec`](crate::render_resource::BufferVec)
/// * [`Texture`](crate::render_resource::Texture) /// * [`Texture`](crate::render_resource::Texture)
/// ///

View file

@ -22,9 +22,10 @@ use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferUsa
/// (vectors), or structures with fields that are vectors. /// (vectors), or structures with fields that are vectors.
/// ///
/// Other options for storing GPU-accessible data are: /// Other options for storing GPU-accessible data are:
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) /// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) /// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
/// * [`BufferVec`](crate::render_resource::BufferVec) /// * [`BufferVec`](crate::render_resource::BufferVec)
/// * [`Texture`](crate::render_resource::Texture) /// * [`Texture`](crate::render_resource::Texture)
/// ///
@ -151,6 +152,8 @@ impl<T: ShaderType + WriteInto> UniformBuffer<T> {
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) /// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) /// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) /// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
/// * [`BufferVec`](crate::render_resource::BufferVec)
/// * [`Texture`](crate::render_resource::Texture) /// * [`Texture`](crate::render_resource::Texture)
/// ///
/// [std140 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-uniform /// [std140 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-uniform
@ -177,6 +180,17 @@ impl<T: ShaderType> Default for DynamicUniformBuffer<T> {
} }
impl<T: ShaderType + WriteInto> DynamicUniformBuffer<T> { impl<T: ShaderType + WriteInto> DynamicUniformBuffer<T> {
pub fn new_with_alignment(alignment: u64) -> Self {
Self {
scratch: DynamicUniformBufferWrapper::new_with_alignment(Vec::new(), alignment),
buffer: None,
label: None,
changed: false,
buffer_usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM,
_marker: PhantomData,
}
}
#[inline] #[inline]
pub fn buffer(&self) -> Option<&Buffer> { pub fn buffer(&self) -> Option<&Buffer> {
self.buffer.as_ref() self.buffer.as_ref()

View file

@ -82,7 +82,7 @@ impl FromWorld for ImageTextureLoader {
let supported_compressed_formats = match world.get_resource::<RenderDevice>() { let supported_compressed_formats = match world.get_resource::<RenderDevice>() {
Some(render_device) => CompressedImageFormats::from_features(render_device.features()), Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::all(), None => CompressedImageFormats::NONE,
}; };
Self { Self {
supported_compressed_formats, supported_compressed_formats,

View file

@ -70,19 +70,6 @@ impl ImagePlugin {
impl Plugin for ImagePlugin { impl Plugin for ImagePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
#[cfg(any(
feature = "png",
feature = "dds",
feature = "tga",
feature = "jpeg",
feature = "bmp",
feature = "basis-universal",
feature = "ktx2",
))]
{
app.init_asset_loader::<ImageTextureLoader>();
}
#[cfg(feature = "exr")] #[cfg(feature = "exr")]
{ {
app.init_asset_loader::<ExrTextureLoader>(); app.init_asset_loader::<ExrTextureLoader>();
@ -112,6 +99,19 @@ impl Plugin for ImagePlugin {
} }
fn finish(&self, app: &mut App) { fn finish(&self, app: &mut App) {
#[cfg(any(
feature = "png",
feature = "dds",
feature = "tga",
feature = "jpeg",
feature = "bmp",
feature = "basis-universal",
feature = "ktx2",
))]
{
app.init_asset_loader::<ImageTextureLoader>();
}
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
let default_sampler = { let default_sampler = {
let device = render_app.world.resource::<RenderDevice>(); let device = render_app.world.resource::<RenderDevice>();

View file

@ -367,7 +367,7 @@ pub fn check_visibility(
let view_mask = maybe_view_mask.copied().unwrap_or_default(); let view_mask = maybe_view_mask.copied().unwrap_or_default();
visible_entities.entities.clear(); visible_entities.entities.clear();
visible_aabb_query.par_iter_mut().for_each_mut( visible_aabb_query.par_iter_mut().for_each(
|( |(
entity, entity,
mut computed_visibility, mut computed_visibility,
@ -412,7 +412,7 @@ pub fn check_visibility(
}, },
); );
visible_no_aabb_query.par_iter_mut().for_each_mut( visible_no_aabb_query.par_iter_mut().for_each(
|(entity, mut computed_visibility, maybe_entity_mask)| { |(entity, mut computed_visibility, maybe_entity_mask)| {
// skip computing visibility for entities that are configured to be hidden. is_visible_in_view has already been set to false // skip computing visibility for entities that are configured to be hidden. is_visible_in_view has already been set to false
// in visibility_propagate_system // in visibility_propagate_system

View file

@ -266,6 +266,7 @@ pub fn prepare_windows(
usage: wgpu::TextureUsages::RENDER_ATTACHMENT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
present_mode: match window.present_mode { present_mode: match window.present_mode {
PresentMode::Fifo => wgpu::PresentMode::Fifo, PresentMode::Fifo => wgpu::PresentMode::Fifo,
PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed,
PresentMode::Mailbox => wgpu::PresentMode::Mailbox, PresentMode::Mailbox => wgpu::PresentMode::Mailbox,
PresentMode::Immediate => wgpu::PresentMode::Immediate, PresentMode::Immediate => wgpu::PresentMode::Immediate,
PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync, PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync,

View file

@ -29,7 +29,7 @@ pub fn sync_simple_transforms(
query query
.p0() .p0()
.par_iter_mut() .par_iter_mut()
.for_each_mut(|(transform, mut global_transform)| { .for_each(|(transform, mut global_transform)| {
*global_transform = GlobalTransform::from(*transform); *global_transform = GlobalTransform::from(*transform);
}); });
// Update orphaned entities. // Update orphaned entities.
@ -59,7 +59,7 @@ pub fn propagate_transforms(
orphaned_entities.clear(); orphaned_entities.clear();
orphaned_entities.extend(orphaned.iter()); orphaned_entities.extend(orphaned.iter());
orphaned_entities.sort_unstable(); orphaned_entities.sort_unstable();
root_query.par_iter_mut().for_each_mut( root_query.par_iter_mut().for_each(
|(entity, children, transform, mut global_transform)| { |(entity, children, transform, mut global_transform)| {
let changed = transform.is_changed() || global_transform.is_added() || orphaned_entities.binary_search(&entity).is_ok(); let changed = transform.is_changed() || global_transform.is_added() || orphaned_entities.binary_search(&entity).is_ok();
if changed { if changed {

View file

@ -58,6 +58,7 @@ pub const UI_SHADER_HANDLE: HandleUntyped =
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum RenderUiSystem { pub enum RenderUiSystem {
ExtractNode, ExtractNode,
ExtractAtlasNode,
} }
pub fn build_ui_render(app: &mut App) { pub fn build_ui_render(app: &mut App) {
@ -81,10 +82,12 @@ pub fn build_ui_render(app: &mut App) {
extract_default_ui_camera_view::<Camera2d>, extract_default_ui_camera_view::<Camera2d>,
extract_default_ui_camera_view::<Camera3d>, extract_default_ui_camera_view::<Camera3d>,
extract_uinodes.in_set(RenderUiSystem::ExtractNode), extract_uinodes.in_set(RenderUiSystem::ExtractNode),
extract_atlas_uinodes.after(RenderUiSystem::ExtractNode), extract_atlas_uinodes
extract_uinode_borders.after(RenderUiSystem::ExtractNode), .in_set(RenderUiSystem::ExtractAtlasNode)
.after(RenderUiSystem::ExtractNode),
extract_uinode_borders.after(RenderUiSystem::ExtractAtlasNode),
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
extract_text_uinodes.after(RenderUiSystem::ExtractNode), extract_text_uinodes.after(RenderUiSystem::ExtractAtlasNode),
), ),
) )
.add_systems( .add_systems(

View file

@ -141,7 +141,8 @@ impl Plugin for WindowPlugin {
.register_type::<InternalWindowState>() .register_type::<InternalWindowState>()
.register_type::<MonitorSelection>() .register_type::<MonitorSelection>()
.register_type::<WindowResizeConstraints>() .register_type::<WindowResizeConstraints>()
.register_type::<WindowTheme>(); .register_type::<WindowTheme>()
.register_type::<EnabledButtons>();
// Register `PathBuf` as it's used by `FileDragAndDrop` // Register `PathBuf` as it's used by `FileDragAndDrop`
app.register_type::<PathBuf>(); app.register_type::<PathBuf>();

View file

@ -126,13 +126,21 @@ pub struct Window {
/// Note: This does not stop the program from fullscreening/setting /// Note: This does not stop the program from fullscreening/setting
/// the size programmatically. /// the size programmatically.
pub resizable: bool, pub resizable: bool,
/// Specifies which window control buttons should be enabled.
///
/// ## Platform-specific
///
/// **`iOS`**, **`Android`**, and the **`Web`** do not have window control buttons.
///
/// On some **`Linux`** environments these values have no effect.
pub enabled_buttons: EnabledButtons,
/// Should the window have decorations enabled? /// Should the window have decorations enabled?
/// ///
/// (Decorations are the minimize, maximize, and close buttons on desktop apps) /// (Decorations are the minimize, maximize, and close buttons on desktop apps)
/// ///
// ## Platform-specific /// ## Platform-specific
// ///
// **`iOS`**, **`Android`**, and the **`Web`** do not have decorations. /// **`iOS`**, **`Android`**, and the **`Web`** do not have decorations.
pub decorations: bool, pub decorations: bool,
/// Should the window be transparent? /// Should the window be transparent?
/// ///
@ -221,6 +229,7 @@ impl Default for Window {
ime_enabled: Default::default(), ime_enabled: Default::default(),
ime_position: Default::default(), ime_position: Default::default(),
resizable: true, resizable: true,
enabled_buttons: Default::default(),
decorations: true, decorations: true,
transparent: false, transparent: false,
focused: true, focused: true,
@ -801,6 +810,7 @@ pub enum MonitorSelection {
/// [`Immediate`] or [`Mailbox`] will panic if not supported by the platform. /// [`Immediate`] or [`Mailbox`] will panic if not supported by the platform.
/// ///
/// [`Fifo`]: PresentMode::Fifo /// [`Fifo`]: PresentMode::Fifo
/// [`FifoRelaxed`]: PresentMode::FifoRelaxed
/// [`Immediate`]: PresentMode::Immediate /// [`Immediate`]: PresentMode::Immediate
/// [`Mailbox`]: PresentMode::Mailbox /// [`Mailbox`]: PresentMode::Mailbox
/// [`AutoVsync`]: PresentMode::AutoVsync /// [`AutoVsync`]: PresentMode::AutoVsync
@ -819,30 +829,67 @@ pub enum PresentMode {
/// Chooses FifoRelaxed -> Fifo based on availability. /// Chooses FifoRelaxed -> Fifo based on availability.
/// ///
/// Because of the fallback behavior, it is supported everywhere. /// Because of the fallback behavior, it is supported everywhere.
AutoVsync = 0, AutoVsync = 0, // NOTE: The explicit ordinal values mirror wgpu.
/// Chooses Immediate -> Mailbox -> Fifo (on web) based on availability. /// Chooses Immediate -> Mailbox -> Fifo (on web) based on availability.
/// ///
/// Because of the fallback behavior, it is supported everywhere. /// Because of the fallback behavior, it is supported everywhere.
AutoNoVsync = 1, AutoNoVsync = 1,
/// The presentation engine does **not** wait for a vertical blanking period and /// Presentation frames are kept in a First-In-First-Out queue approximately 3 frames
/// the request is presented immediately. This is a low-latency presentation mode, /// long. Every vertical blanking period, the presentation engine will pop a frame
/// but visible tearing may be observed. Not optimal for mobile. /// off the queue to display. If there is no frame to display, it will present the same
/// frame again until the next vblank.
/// ///
/// Selecting this variant will panic if not supported, it is preferred to use /// When a present command is executed on the gpu, the presented image is added on the queue.
/// [`PresentMode::AutoNoVsync`].
Immediate = 2,
/// The presentation engine waits for the next vertical blanking period to update
/// the current image, but frames may be submitted without delay. This is a low-latency
/// presentation mode and visible tearing will **not** be observed. Not optimal for mobile.
/// ///
/// Selecting this variant will panic if not supported, it is preferred to use /// No tearing will be observed.
/// [`PresentMode::AutoNoVsync`]. ///
Mailbox = 3, /// Calls to get_current_texture will block until there is a spot in the queue.
/// The presentation engine waits for the next vertical blanking period to update ///
/// the current image. The framerate will be capped at the display refresh rate, /// Supported on all platforms.
/// corresponding to the `VSync`. Tearing cannot be observed. Optimal for mobile. ///
/// If you don't know what mode to choose, choose this mode. This is traditionally called "Vsync On".
#[default] #[default]
Fifo = 4, // NOTE: The explicit ordinal values mirror wgpu. Fifo = 2,
/// Presentation frames are kept in a First-In-First-Out queue approximately 3 frames
/// long. Every vertical blanking period, the presentation engine will pop a frame
/// off the queue to display. If there is no frame to display, it will present the
/// same frame until there is a frame in the queue. The moment there is a frame in the
/// queue, it will immediately pop the frame off the queue.
///
/// When a present command is executed on the gpu, the presented image is added on the queue.
///
/// Tearing will be observed if frames last more than one vblank as the front buffer.
///
/// Calls to get_current_texture will block until there is a spot in the queue.
///
/// Supported on AMD on Vulkan.
///
/// This is traditionally called "Adaptive Vsync"
FifoRelaxed = 3,
/// Presentation frames are not queued at all. The moment a present command
/// is executed on the GPU, the presented image is swapped onto the front buffer
/// immediately.
///
/// Tearing can be observed.
///
/// Supported on most platforms except older DX12 and Wayland.
///
/// This is traditionally called "Vsync Off".
Immediate = 4,
/// Presentation frames are kept in a single-frame queue. Every vertical blanking period,
/// the presentation engine will pop a frame from the queue. If there is no frame to display,
/// it will present the same frame again until the next vblank.
///
/// When a present command is executed on the gpu, the frame will be put into the queue.
/// If there was already a frame in the queue, the new frame will _replace_ the old frame
/// on the queue.
///
/// No tearing will be observed.
///
/// Supported on DX11/12 on Windows 10, NVidia on Vulkan and Wayland on Vulkan.
///
/// This is traditionally called "Fast Vsync"
Mailbox = 5,
} }
/// Specifies how the alpha channel of the textures should be handled during compositing, for a [`Window`]. /// Specifies how the alpha channel of the textures should be handled during compositing, for a [`Window`].
@ -963,3 +1010,42 @@ pub enum WindowTheme {
/// Use the dark variant. /// Use the dark variant.
Dark, Dark,
} }
/// Specifies which [`Window`] control buttons should be enabled.
///
/// ## Platform-specific
///
/// **`iOS`**, **`Android`**, and the **`Web`** do not have window control buttons.
///
/// On some **`Linux`** environments these values have no effect.
#[derive(Debug, Copy, Clone, PartialEq, Reflect)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[reflect(Debug, PartialEq, Default)]
pub struct EnabledButtons {
/// Enables the functionality of the minimize button.
pub minimize: bool,
/// Enables the functionality of the maximize button.
///
/// macOS note: When [`Window`] `resizable` member is set to `false`
/// the maximize button will be disabled regardless of this value.
/// Additionaly, when `resizable` is set to `true` the window will
/// be maximized when its bar is double-clicked regardless of whether
/// the maximize button is enabled or not.
pub maximize: bool,
/// Enables the functionality of the close button.
pub close: bool,
}
impl Default for EnabledButtons {
fn default() -> Self {
Self {
minimize: true,
maximize: true,
close: true,
}
}
}

View file

@ -6,7 +6,7 @@ use bevy_input::{
ButtonState, ButtonState,
}; };
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_window::{CursorIcon, WindowLevel, WindowTheme}; use bevy_window::{CursorIcon, EnabledButtons, WindowLevel, WindowTheme};
pub fn convert_keyboard_input( pub fn convert_keyboard_input(
keyboard_input: &winit::event::KeyboardInput, keyboard_input: &winit::event::KeyboardInput,
@ -293,3 +293,17 @@ pub fn convert_window_theme(theme: WindowTheme) -> winit::window::Theme {
WindowTheme::Dark => winit::window::Theme::Dark, WindowTheme::Dark => winit::window::Theme::Dark,
} }
} }
pub fn convert_enabled_buttons(enabled_buttons: EnabledButtons) -> winit::window::WindowButtons {
let mut window_buttons = winit::window::WindowButtons::empty();
if enabled_buttons.minimize {
window_buttons.insert(winit::window::WindowButtons::MINIMIZE);
}
if enabled_buttons.maximize {
window_buttons.insert(winit::window::WindowButtons::MAXIMIZE);
}
if enabled_buttons.close {
window_buttons.insert(winit::window::WindowButtons::CLOSE);
}
window_buttons
}

View file

@ -23,7 +23,10 @@ use winit::{
use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR}; use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR};
use crate::{ use crate::{
accessibility::{AccessKitAdapters, WinitActionHandlers}, accessibility::{AccessKitAdapters, WinitActionHandlers},
converters::{self, convert_window_level, convert_window_theme, convert_winit_theme}, converters::{
self, convert_enabled_buttons, convert_window_level, convert_window_theme,
convert_winit_theme,
},
get_best_videomode, get_fitting_videomode, WinitWindows, get_best_videomode, get_fitting_videomode, WinitWindows,
}; };
@ -222,6 +225,10 @@ pub(crate) fn changed_window(
winit_window.set_resizable(window.resizable); winit_window.set_resizable(window.resizable);
} }
if window.enabled_buttons != cache.window.enabled_buttons {
winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons));
}
if window.resize_constraints != cache.window.resize_constraints { if window.resize_constraints != cache.window.resize_constraints {
let constraints = window.resize_constraints.check_constraints(); let constraints = window.resize_constraints.check_constraints();
let min_inner_size = LogicalSize { let min_inner_size = LogicalSize {

View file

@ -18,7 +18,7 @@ use winit::{
use crate::{ use crate::{
accessibility::{AccessKitAdapters, WinitActionHandler, WinitActionHandlers}, accessibility::{AccessKitAdapters, WinitActionHandler, WinitActionHandlers},
converters::{convert_window_level, convert_window_theme}, converters::{convert_enabled_buttons, convert_window_level, convert_window_theme},
}; };
/// A resource which maps window entities to [`winit`] library windows. /// A resource which maps window entities to [`winit`] library windows.
@ -94,6 +94,7 @@ impl WinitWindows {
.with_window_level(convert_window_level(window.window_level)) .with_window_level(convert_window_level(window.window_level))
.with_theme(window.window_theme.map(convert_window_theme)) .with_theme(window.window_theme.map(convert_window_theme))
.with_resizable(window.resizable) .with_resizable(window.resizable)
.with_enabled_buttons(convert_enabled_buttons(window.enabled_buttons))
.with_decorations(window.decorations) .with_decorations(window.decorations)
.with_transparent(window.transparent); .with_transparent(window.transparent);

View file

@ -3,7 +3,7 @@
//! Note that this uses [`Text2dBundle`] to display text alongside your other entities in a 2D scene. //! Note that this uses [`Text2dBundle`] to display text alongside your other entities in a 2D scene.
//! //!
//! For an example on how to render text as part of a user interface, independent from the world //! For an example on how to render text as part of a user interface, independent from the world
//! viewport, you may want to look at `2d/contributors.rs` or `ui/text.rs`. //! viewport, you may want to look at `games/contributors.rs` or `ui/text.rs`.
use bevy::{ use bevy::{
prelude::*, prelude::*,

View file

@ -34,7 +34,7 @@ fn move_system(mut sprites: Query<(&mut Transform, &Velocity)>) {
// to use or not use ParallelIterator over a normal Iterator. // to use or not use ParallelIterator over a normal Iterator.
sprites sprites
.par_iter_mut() .par_iter_mut()
.for_each_mut(|(mut transform, velocity)| { .for_each(|(mut transform, velocity)| {
transform.translation += velocity.extend(0.0); transform.translation += velocity.extend(0.0);
}); });
} }
@ -54,7 +54,7 @@ fn bounce_system(windows: Query<&Window>, mut sprites: Query<(&Transform, &mut V
sprites sprites
.par_iter_mut() .par_iter_mut()
.batching_strategy(BatchingStrategy::fixed(32)) .batching_strategy(BatchingStrategy::fixed(32))
.for_each_mut(|(transform, mut v)| { .for_each(|(transform, mut v)| {
if !(left < transform.translation.x if !(left < transform.translation.x
&& transform.translation.x < right && transform.translation.x < right
&& bottom < transform.translation.y && bottom < transform.translation.y

View file

@ -318,8 +318,8 @@ impl FromWorld for PostProcessPipeline {
write_mask: ColorWrites::ALL, write_mask: ColorWrites::ALL,
})], })],
}), }),
// All of the following property are not important for this effect so just use the default values. // All of the following properties are not important for this effect so just use the default values.
// This struct doesn't have the Default trai implemented because not all field can have a default value. // This struct doesn't have the Default trait implemented because not all field can have a default value.
primitive: PrimitiveState::default(), primitive: PrimitiveState::default(),
depth_stencil: None, depth_stencil: None,
multisample: MultisampleState::default(), multisample: MultisampleState::default(),

View file

@ -17,7 +17,7 @@ fn main() {
app.add_plugins(( app.add_plugins((
DefaultPlugins.set(WindowPlugin { DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window { primary_window: Some(Window {
present_mode: PresentMode::Immediate, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}), }),
..default() ..default()

View file

@ -14,7 +14,7 @@ fn main() {
.add_plugins(( .add_plugins((
DefaultPlugins.set(WindowPlugin { DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window { primary_window: Some(Window {
present_mode: PresentMode::Immediate, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}), }),
..default() ..default()

View file

@ -34,6 +34,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Accepts a `String` or any type that converts into a `String`, such as `&str` // Accepts a `String` or any type that converts into a `String`, such as `&str`
"hello\nbevy!", "hello\nbevy!",
TextStyle { TextStyle {
// This font is loaded and will be used instead of the default font.
font: asset_server.load("fonts/FiraSans-Bold.ttf"), font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 100.0, font_size: 100.0,
color: Color::WHITE, color: Color::WHITE,
@ -56,15 +57,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
TextSection::new( TextSection::new(
"FPS: ", "FPS: ",
TextStyle { TextStyle {
// This font is loaded and will be used instead of the default font.
font: asset_server.load("fonts/FiraSans-Bold.ttf"), font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 60.0, font_size: 60.0,
color: Color::WHITE, color: Color::WHITE,
}, },
), ),
TextSection::from_style(TextStyle { TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 60.0, font_size: 60.0,
color: Color::GOLD, color: Color::GOLD,
// If no font is specified, it will use the default font.
..default()
}), }),
]), ]),
FpsText, FpsText,

View file

@ -20,6 +20,10 @@ fn main() {
// Tells wasm not to override default event handling, like F5, Ctrl+R etc. // Tells wasm not to override default event handling, like F5, Ctrl+R etc.
prevent_default_event_handling: false, prevent_default_event_handling: false,
window_theme: Some(WindowTheme::Dark), window_theme: Some(WindowTheme::Dark),
enabled_buttons: bevy::window::EnabledButtons {
maximize: false,
..Default::default()
},
..default() ..default()
}), }),
..default() ..default()
@ -34,6 +38,7 @@ fn main() {
toggle_theme, toggle_theme,
toggle_cursor, toggle_cursor,
toggle_vsync, toggle_vsync,
toggle_window_controls,
cycle_cursor_icon, cycle_cursor_icon,
switch_level, switch_level,
), ),
@ -76,6 +81,31 @@ fn switch_level(input: Res<Input<KeyCode>>, mut windows: Query<&mut Window>) {
} }
} }
/// This system toggles the window controls when pressing buttons 1, 2 and 3
///
/// This feature only works on some platforms. Please check the
/// [documentation](https://docs.rs/bevy/latest/bevy/prelude/struct.Window.html#structfield.enabled_buttons)
/// for more details.
fn toggle_window_controls(input: Res<Input<KeyCode>>, mut windows: Query<&mut Window>) {
let toggle_minimize = input.just_pressed(KeyCode::Key1);
let toggle_maximize = input.just_pressed(KeyCode::Key2);
let toggle_close = input.just_pressed(KeyCode::Key3);
if toggle_minimize || toggle_maximize || toggle_close {
let mut window = windows.single_mut();
if toggle_minimize {
window.enabled_buttons.minimize = !window.enabled_buttons.minimize;
}
if toggle_maximize {
window.enabled_buttons.maximize = !window.enabled_buttons.maximize;
}
if toggle_close {
window.enabled_buttons.close = !window.enabled_buttons.close;
}
}
}
/// This system will then change the title during execution /// This system will then change the title during execution
fn change_title(mut windows: Query<&mut Window>, time: Res<Time>) { fn change_title(mut windows: Query<&mut Window>, time: Res<Time>) {
let mut window = windows.single_mut(); let mut window = windows.single_mut();

View file

@ -53,7 +53,7 @@ enum Action {
/// Path to the folder where the content should be created /// Path to the folder where the content should be created
content_folder: String, content_folder: String,
#[arg(value_enum, long, default_value_t = WebApi::Webgl2)] #[arg(value_enum, long, default_value_t = WebApi::Webgpu)]
/// Which API to use for rendering /// Which API to use for rendering
api: WebApi, api: WebApi,
}, },