Merge branch 'main' into transmission

This commit is contained in:
Marco Buono 2023-04-24 23:36:48 -03:00
commit 3862e82df5
171 changed files with 6519 additions and 1573 deletions

View file

@ -259,9 +259,9 @@ jobs:
id: missing-metadata
run: cargo run -p build-templated-pages -- check-missing examples
- name: check for missing update
id: missing-update
run: cargo run -p build-templated-pages -- update examples
- name: Check for modified files
id: missing-update
run: |
echo "if this step fails, run the following command and commit the changed file on your PR."
echo " > cargo run -p build-templated-pages -- update examples"
@ -293,9 +293,9 @@ jobs:
id: missing-features
run: cargo run -p build-templated-pages -- check-missing features
- name: check for missing update
id: missing-update
run: cargo run -p build-templated-pages -- update features
- name: Check for modified files
id: missing-update
run: |
echo "if this step fails, run the following command and commit the changed file on your PR."
echo " > cargo run -p build-templated-pages -- update features"

View file

@ -265,6 +265,7 @@ Examples in Bevy should be:
4. **Minimal:** They should be no larger or complex than is needed to meet the goals of the example.
When you add a new example, be sure to update `examples/README.md` with the new example and add it to the root `Cargo.toml` file.
Run `cargo run -p build-templated-pages -- build-example-page` to do this automatically.
Use a generous sprinkling of keywords in your description: these are commonly used to search for a specific example.
See the [example style guide](.github/contributing/example_style_guide.md) to help make sure the style of your example matches what we're already using.

View file

@ -25,3 +25,4 @@
* Low poly fox [by PixelMannen](https://opengameart.org/content/fox-and-shiba) (CC0 1.0 Universal)
* Rigging and animation [by @tomkranis on Sketchfab](https://sketchfab.com/models/371dea88d7e04a76af5763f2a36866bc) ([CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/))
* FiraMono by The Mozilla Foundation and Telefonica S.A (SIL Open Font License, Version 1.1: assets/fonts/FiraMono-LICENSE)
* Barycentric from [mk_bary_gltf](https://github.com/komadori/mk_bary_gltf) (MIT OR Apache-2.0)

View file

@ -52,6 +52,7 @@ default = [
"bevy_gizmos",
"android_shared_stdcxx",
"tonemapping_luts",
"default_font",
]
# Force dynamic linking, which improves iterative compile times
@ -108,6 +109,9 @@ trace_chrome = ["trace", "bevy_internal/trace_chrome"]
# Tracing support, exposing a port for Tracy
trace_tracy = ["trace", "bevy_internal/trace_tracy"]
# Tracing support, with memory profiling, exposing a port for Tracy
trace_tracy_memory = ["trace", "bevy_internal/trace_tracy", "bevy_internal/trace_tracy_memory"]
# Tracing support
trace = ["bevy_internal/trace"]
@ -222,6 +226,9 @@ accesskit_unix = ["bevy_internal/accesskit_unix"]
# Enable assertions to check the validity of parameters passed to glam
glam_assert = ["bevy_internal/glam_assert"]
# Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase
default_font = ["bevy_internal/default_font"]
[dependencies]
bevy_dylib = { path = "crates/bevy_dylib", version = "0.11.0-dev", default-features = false, optional = true }
bevy_internal = { path = "crates/bevy_internal", version = "0.11.0-dev", default-features = false }
@ -319,6 +326,16 @@ description = "Renders a rectangle, circle, and hexagon"
category = "2D Rendering"
wasm = true
[[example]]
name = "custom_gltf_vertex_attribute"
path = "examples/2d/custom_gltf_vertex_attribute.rs"
[package.metadata.example.custom_gltf_vertex_attribute]
name = "Custom glTF vertex attribute 2D"
description = "Renders a glTF mesh in 2D with a custom vertex attribute"
category = "2D Rendering"
wasm = true
[[example]]
name = "2d_gizmos"
path = "examples/2d/2d_gizmos.rs"
@ -729,6 +746,16 @@ description = "Create and play an animation defined by code that operates on the
category = "Animation"
wasm = true
[[example]]
name = "cubic_curve"
path = "examples/animation/cubic_curve.rs"
[package.metadata.example.cubic_curve]
name = "Cubic Curve"
description = "Bezier curve example showing a cube following a cubic curve"
category = "Animation"
wasm = true
[[example]]
name = "custom_skinned_mesh"
path = "examples/animation/custom_skinned_mesh.rs"
@ -1257,6 +1284,16 @@ description = "Iterates and prints gamepad input and connection events"
category = "Input"
wasm = false
[[example]]
name = "gamepad_rumble"
path = "examples/input/gamepad_rumble.rs"
[package.metadata.example.gamepad_rumble]
name = "Gamepad Rumble"
description = "Shows how to rumble a gamepad using force feedback"
category = "Input"
wasm = false
[[example]]
name = "keyboard_input"
path = "examples/input/keyboard_input.rs"
@ -1755,6 +1792,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
category = "UI (User Interface)"
wasm = true
[[example]]
name = "overflow"
path = "examples/ui/overflow.rs"
[package.metadata.example.overflow]
name = "Overflow"
description = "Simple example demonstrating overflow behavior"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "overflow_debug"
path = "examples/ui/overflow_debug.rs"
@ -1775,6 +1822,16 @@ description = "Showcases the RelativeCursorPosition component"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "size_constraints"
path = "examples/ui/size_constraints.rs"
[package.metadata.example.size_constraints]
name = "Size Constraints"
description = "Demonstrates how the to use the size constraints to control the size of a UI node."
category = "UI (User Interface)"
wasm = true
[[example]]
name = "text"
path = "examples/ui/text.rs"
@ -1803,7 +1860,28 @@ path = "examples/ui/flex_layout.rs"
name = "Flex Layout"
description = "Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text"
category = "UI (User Interface)"
wasm = false
wasm = true
[[example]]
name = "text_wrap_debug"
path = "examples/ui/text_wrap_debug.rs"
[package.metadata.example.text_wrap_debug]
name = "Text Wrap Debug"
description = "Demonstrates text wrapping"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "grid"
path = "examples/ui/grid.rs"
[package.metadata.example.grid]
name = "CSS Grid"
description = "An example for CSS Grid layout"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "transparency_ui"
@ -1886,6 +1964,16 @@ description = "Illustrates how to customize the default window settings"
category = "Window"
wasm = true
[[example]]
name = "screenshot"
path = "examples/window/screenshot.rs"
[package.metadata.example.screenshot]
name = "Screenshot"
description = "Shows how to save screenshots to disk"
category = "Window"
wasm = false
[[example]]
name = "transparent_window"
path = "examples/window/transparent_window.rs"

View file

@ -0,0 +1,80 @@
{
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"count": 4,
"componentType": 5126,
"type": "VEC3",
"min": [
-1.0,
-1.0,
0.0
],
"max": [
1.0,
1.0,
0.0
]
},
{
"bufferView": 0,
"byteOffset": 12,
"count": 4,
"componentType": 5126,
"type": "VEC4"
},
{
"bufferView": 0,
"byteOffset": 28,
"count": 4,
"componentType": 5126,
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 0,
"count": 6,
"componentType": 5123,
"type": "SCALAR"
}
],
"asset": {
"version": "2.0"
},
"buffers": [
{
"byteLength": 172,
"uri": "data:application/gltf-buffer;base64,AACAvwAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAgD8AAIC/AAAAAAAAAD8AAAA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIC/AACAPwAAAAAAAAA/AAAAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AACAPwAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAQACAAIAAQADAA=="
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 160,
"byteOffset": 0,
"byteStride": 40,
"target": 34962
},
{
"buffer": 0,
"byteLength": 12,
"byteOffset": 160,
"target": 34962
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 0,
"COLOR_0": 1,
"__BARYCENTRIC": 2
},
"indices": 3
}
]
}
]
}

View file

@ -0,0 +1,36 @@
#import bevy_sprite::mesh2d_view_bindings
#import bevy_sprite::mesh2d_bindings
#import bevy_sprite::mesh2d_functions
struct Vertex {
@location(0) position: vec3<f32>,
@location(1) color: vec4<f32>,
@location(2) barycentric: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) barycentric: vec3<f32>,
};
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
out.clip_position = mesh2d_position_local_to_clip(mesh.model, vec4<f32>(vertex.position, 1.0));
out.color = vertex.color;
out.barycentric = vertex.barycentric;
return out;
}
struct FragmentInput {
@location(0) color: vec4<f32>,
@location(1) barycentric: vec3<f32>,
};
@fragment
fn fragment(input: FragmentInput) -> @location(0) vec4<f32> {
let d = min(input.barycentric.x, min(input.barycentric.y, input.barycentric.z));
let t = 0.05 * (0.85 + sin(5.0 * globals.time));
return mix(vec4(1.0,1.0,1.0,1.0), input.color, smoothstep(t, t+0.01, d));
}

View file

@ -9,6 +9,9 @@ criterion_group!(
benches,
concrete_struct_apply,
concrete_struct_field,
concrete_struct_type_info,
concrete_struct_clone,
dynamic_struct_clone,
dynamic_struct_apply,
dynamic_struct_get_field,
dynamic_struct_insert,
@ -110,6 +113,128 @@ fn concrete_struct_apply(criterion: &mut Criterion) {
}
}
fn concrete_struct_type_info(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("concrete_struct_type_info");
group.warm_up_time(WARM_UP_TIME);
group.measurement_time(MEASUREMENT_TIME);
let structs: [(Box<dyn Struct>, Box<dyn Struct>); 5] = [
(
Box::new(Struct1::default()),
Box::new(GenericStruct1::<u32>::default()),
),
(
Box::new(Struct16::default()),
Box::new(GenericStruct16::<u32>::default()),
),
(
Box::new(Struct32::default()),
Box::new(GenericStruct32::<u32>::default()),
),
(
Box::new(Struct64::default()),
Box::new(GenericStruct64::<u32>::default()),
),
(
Box::new(Struct128::default()),
Box::new(GenericStruct128::<u32>::default()),
),
];
for (standard, generic) in structs {
let field_count = standard.field_len();
group.bench_with_input(
BenchmarkId::new("NonGeneric", field_count),
&standard,
|bencher, s| {
bencher.iter(|| black_box(s.get_type_info()));
},
);
group.bench_with_input(
BenchmarkId::new("Generic", field_count),
&generic,
|bencher, s| {
bencher.iter(|| black_box(s.get_type_info()));
},
);
}
}
fn concrete_struct_clone(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("concrete_struct_clone");
group.warm_up_time(WARM_UP_TIME);
group.measurement_time(MEASUREMENT_TIME);
let structs: [(Box<dyn Struct>, Box<dyn Struct>); 5] = [
(
Box::new(Struct1::default()),
Box::new(GenericStruct1::<u32>::default()),
),
(
Box::new(Struct16::default()),
Box::new(GenericStruct16::<u32>::default()),
),
(
Box::new(Struct32::default()),
Box::new(GenericStruct32::<u32>::default()),
),
(
Box::new(Struct64::default()),
Box::new(GenericStruct64::<u32>::default()),
),
(
Box::new(Struct128::default()),
Box::new(GenericStruct128::<u32>::default()),
),
];
for (standard, generic) in structs {
let field_count = standard.field_len();
group.bench_with_input(
BenchmarkId::new("NonGeneric", field_count),
&standard,
|bencher, s| {
bencher.iter(|| black_box(s.clone_dynamic()));
},
);
group.bench_with_input(
BenchmarkId::new("Generic", field_count),
&generic,
|bencher, s| {
bencher.iter(|| black_box(s.clone_dynamic()));
},
);
}
}
fn dynamic_struct_clone(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("dynamic_struct_clone");
group.warm_up_time(WARM_UP_TIME);
group.measurement_time(MEASUREMENT_TIME);
let structs: [Box<dyn Struct>; 5] = [
Box::new(Struct1::default().clone_dynamic()),
Box::new(Struct16::default().clone_dynamic()),
Box::new(Struct32::default().clone_dynamic()),
Box::new(Struct64::default().clone_dynamic()),
Box::new(Struct128::default().clone_dynamic()),
];
for s in structs {
let field_count = s.field_len();
group.bench_with_input(
BenchmarkId::from_parameter(field_count),
&s,
|bencher, s| {
bencher.iter(|| black_box(s.clone_dynamic()));
},
);
}
}
fn dynamic_struct_apply(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("dynamic_struct_apply");
group.warm_up_time(WARM_UP_TIME);
@ -228,6 +353,11 @@ fn dynamic_struct_get_field(criterion: &mut Criterion) {
}
}
#[derive(Clone, Default, Reflect)]
struct Struct1 {
field_0: u32,
}
#[derive(Clone, Default, Reflect)]
struct Struct16 {
field_0: u32,
@ -483,3 +613,264 @@ struct Struct128 {
field_126: u32,
field_127: u32,
}
#[derive(Clone, Default, Reflect)]
struct GenericStruct1<T: Reflect + Default> {
field_0: T,
}
#[derive(Clone, Default, Reflect)]
struct GenericStruct16<T: Reflect + Default> {
field_0: T,
field_1: T,
field_2: T,
field_3: T,
field_4: T,
field_5: T,
field_6: T,
field_7: T,
field_8: T,
field_9: T,
field_10: T,
field_11: T,
field_12: T,
field_13: T,
field_14: T,
field_15: T,
}
#[derive(Clone, Default, Reflect)]
struct GenericStruct32<T: Reflect + Default> {
field_0: T,
field_1: T,
field_2: T,
field_3: T,
field_4: T,
field_5: T,
field_6: T,
field_7: T,
field_8: T,
field_9: T,
field_10: T,
field_11: T,
field_12: T,
field_13: T,
field_14: T,
field_15: T,
field_16: T,
field_17: T,
field_18: T,
field_19: T,
field_20: T,
field_21: T,
field_22: T,
field_23: T,
field_24: T,
field_25: T,
field_26: T,
field_27: T,
field_28: T,
field_29: T,
field_30: T,
field_31: T,
}
#[derive(Clone, Default, Reflect)]
struct GenericStruct64<T: Reflect + Default> {
field_0: T,
field_1: T,
field_2: T,
field_3: T,
field_4: T,
field_5: T,
field_6: T,
field_7: T,
field_8: T,
field_9: T,
field_10: T,
field_11: T,
field_12: T,
field_13: T,
field_14: T,
field_15: T,
field_16: T,
field_17: T,
field_18: T,
field_19: T,
field_20: T,
field_21: T,
field_22: T,
field_23: T,
field_24: T,
field_25: T,
field_26: T,
field_27: T,
field_28: T,
field_29: T,
field_30: T,
field_31: T,
field_32: T,
field_33: T,
field_34: T,
field_35: T,
field_36: T,
field_37: T,
field_38: T,
field_39: T,
field_40: T,
field_41: T,
field_42: T,
field_43: T,
field_44: T,
field_45: T,
field_46: T,
field_47: T,
field_48: T,
field_49: T,
field_50: T,
field_51: T,
field_52: T,
field_53: T,
field_54: T,
field_55: T,
field_56: T,
field_57: T,
field_58: T,
field_59: T,
field_60: T,
field_61: T,
field_62: T,
field_63: T,
}
#[derive(Clone, Default, Reflect)]
struct GenericStruct128<T: Reflect + Default> {
field_0: T,
field_1: T,
field_2: T,
field_3: T,
field_4: T,
field_5: T,
field_6: T,
field_7: T,
field_8: T,
field_9: T,
field_10: T,
field_11: T,
field_12: T,
field_13: T,
field_14: T,
field_15: T,
field_16: T,
field_17: T,
field_18: T,
field_19: T,
field_20: T,
field_21: T,
field_22: T,
field_23: T,
field_24: T,
field_25: T,
field_26: T,
field_27: T,
field_28: T,
field_29: T,
field_30: T,
field_31: T,
field_32: T,
field_33: T,
field_34: T,
field_35: T,
field_36: T,
field_37: T,
field_38: T,
field_39: T,
field_40: T,
field_41: T,
field_42: T,
field_43: T,
field_44: T,
field_45: T,
field_46: T,
field_47: T,
field_48: T,
field_49: T,
field_50: T,
field_51: T,
field_52: T,
field_53: T,
field_54: T,
field_55: T,
field_56: T,
field_57: T,
field_58: T,
field_59: T,
field_60: T,
field_61: T,
field_62: T,
field_63: T,
field_64: T,
field_65: T,
field_66: T,
field_67: T,
field_68: T,
field_69: T,
field_70: T,
field_71: T,
field_72: T,
field_73: T,
field_74: T,
field_75: T,
field_76: T,
field_77: T,
field_78: T,
field_79: T,
field_80: T,
field_81: T,
field_82: T,
field_83: T,
field_84: T,
field_85: T,
field_86: T,
field_87: T,
field_88: T,
field_89: T,
field_90: T,
field_91: T,
field_92: T,
field_93: T,
field_94: T,
field_95: T,
field_96: T,
field_97: T,
field_98: T,
field_99: T,
field_100: T,
field_101: T,
field_102: T,
field_103: T,
field_104: T,
field_105: T,
field_106: T,
field_107: T,
field_108: T,
field_109: T,
field_110: T,
field_111: T,
field_112: T,
field_113: T,
field_114: T,
field_115: T,
field_116: T,
field_117: T,
field_118: T,
field_119: T,
field_120: T,
field_121: T,
field_122: T,
field_123: T,
field_124: T,
field_125: T,
field_126: T,
field_127: T,
}

View file

@ -315,7 +315,7 @@ fn find_bone(
Some(current_entity)
}
/// Verify that there are no ancestors of a given entity that have an `AnimationPlayer`.
/// Verify that there are no ancestors of a given entity that have an [`AnimationPlayer`].
fn verify_no_ancestor_player(
player_parent: Option<&Parent>,
parents: &Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,

View file

@ -160,11 +160,9 @@ impl SubApp {
}
}
/// Runs the `SubApp`'s default schedule.
/// Runs the [`SubApp`]'s default schedule.
pub fn run(&mut self) {
self.app
.world
.run_schedule_ref(&*self.app.main_schedule_label);
self.app.world.run_schedule(&*self.app.main_schedule_label);
self.app.world.clear_trackers();
}
@ -241,7 +239,7 @@ impl App {
{
#[cfg(feature = "trace")]
let _bevy_frame_update_span = info_span!("main app").entered();
self.world.run_schedule_ref(&*self.main_schedule_label);
self.world.run_schedule(&*self.main_schedule_label);
}
for (_label, sub_app) in self.sub_apps.iter_mut() {
#[cfg(feature = "trace")]
@ -674,7 +672,7 @@ impl App {
}
}
/// Boxed variant of `add_plugin`, can be used from a [`PluginGroup`]
/// Boxed variant of [`add_plugin`](App::add_plugin) that can be used from a [`PluginGroup`]
pub(crate) fn add_boxed_plugin(
&mut self,
plugin: Box<dyn Plugin>,

View file

@ -143,7 +143,7 @@ impl Main {
world.resource_scope(|world, order: Mut<MainScheduleOrder>| {
for label in &order.labels {
let _ = world.try_run_schedule_ref(&**label);
let _ = world.try_run_schedule(&**label);
}
});
}

View file

@ -14,7 +14,7 @@ use parking_lot::{Mutex, RwLock};
use std::{path::Path, sync::Arc};
use thiserror::Error;
/// Errors that occur while loading assets with an `AssetServer`.
/// Errors that occur while loading assets with an [`AssetServer`].
#[derive(Error, Debug)]
pub enum AssetServerError {
/// Asset folder is not a directory.

View file

@ -276,7 +276,7 @@ pub trait AddAsset {
where
T: Asset;
/// Registers the asset type `T` using `[App::register]`,
/// Registers the asset type `T` using [`App::register_type`],
/// and adds [`ReflectAsset`] type data to `T` and [`ReflectHandle`] type data to [`Handle<T>`] in the type registry.
///
/// This enables reflection code to access assets. For detailed information, see the docs on [`ReflectAsset`] and [`ReflectHandle`].
@ -296,7 +296,7 @@ pub trait AddAsset {
/// Adds an asset loader `T` using default values.
///
/// The default values may come from the `World` or from `T::default()`.
/// The default values may come from the [`World`] or from `T::default()`.
fn init_asset_loader<T>(&mut self) -> &mut Self
where
T: AssetLoader + FromWorld;
@ -306,7 +306,7 @@ pub trait AddAsset {
/// Internal assets (e.g. shaders) are bundled directly into the app and can't be hot reloaded
/// using the conventional API. See `DebugAssetServerPlugin`.
///
/// The default values may come from the `World` or from `T::default()`.
/// The default values may come from the [`World`] or from `T::default()`.
fn init_debug_asset_loader<T>(&mut self) -> &mut Self
where
T: AssetLoader + FromWorld;
@ -407,7 +407,7 @@ impl AddAsset for App {
/// Loads an internal asset.
///
/// Internal assets (e.g. shaders) are bundled directly into the app and can't be hot reloaded
/// using the conventional API. See `DebugAssetServerPlugin`.
/// using the conventional API. See [`DebugAssetServerPlugin`](crate::debug_asset_server::DebugAssetServerPlugin).
#[cfg(feature = "debug_asset_server")]
#[macro_export]
macro_rules! load_internal_asset {

View file

@ -11,9 +11,9 @@ use std::{
///
/// Implementation details:
///
/// - `load_path` uses the [AssetManager] to load files.
/// - `read_directory` always returns an empty iterator.
/// - `get_metadata` will probably return an error.
/// - [`load_path`](AssetIo::load_path) uses the [`AssetManager`] to load files.
/// - [`read_directory`](AssetIo::read_directory) always returns an empty iterator.
/// - [`get_metadata`](AssetIo::get_metadata) will probably return an error.
/// - Watching for changes is not supported. The watcher methods will do nothing.
///
/// [AssetManager]: https://developer.android.com/reference/android/content/res/AssetManager

View file

@ -82,7 +82,7 @@ impl FileAssetIo {
/// Returns the root directory where assets are loaded from.
///
/// See `get_base_path`.
/// See [`get_base_path`](FileAssetIo::get_base_path).
pub fn root_path(&self) -> &PathBuf {
&self.root_path
}

View file

@ -67,7 +67,7 @@ pub trait AssetIo: Downcast + Send + Sync + 'static {
/// Tells the asset I/O to watch for changes recursively at the provided path.
///
/// No-op if `watch_for_changes` hasn't been called yet.
/// No-op if [`watch_for_changes`](AssetIo::watch_for_changes) hasn't been called yet.
/// Otherwise triggers a reload each time `to_watch` changes.
/// In most cases the asset found at the watched path should be changed,
/// but when an asset depends on data at another path, the asset's path

View file

@ -80,7 +80,7 @@ impl Default for AssetPlugin {
}
impl AssetPlugin {
/// Creates an instance of the platform's default `AssetIo`.
/// Creates an instance of the platform's default [`AssetIo`].
///
/// This is useful when providing a custom `AssetIo` instance that needs to
/// delegate to the default `AssetIo` for the platform.

View file

@ -7,7 +7,8 @@ use crate::{Asset, Assets, Handle, HandleId, HandleUntyped};
/// Type data for the [`TypeRegistry`](bevy_reflect::TypeRegistry) used to operate on reflected [`Asset`]s.
///
/// This type provides similar methods to [`Assets<T>`] like `get`, `add` and `remove`, but can be used in situations where you don't know which asset type `T` you want
/// This type provides similar methods to [`Assets<T>`] like [`get`](ReflectAsset::get),
/// [`add`](ReflectAsset::add) and [`remove`](ReflectAsset::remove), but can be used in situations where you don't know which asset type `T` you want
/// until runtime.
///
/// [`ReflectAsset`] can be obtained via [`TypeRegistration::data`](bevy_reflect::TypeRegistration::data) if the asset was registered using [`register_asset_reflect`](crate::AddAsset::register_asset_reflect).

View file

@ -34,7 +34,7 @@ use std::path::PathBuf;
#[cfg(not(target_arch = "wasm32"))]
use bevy_tasks::tick_global_task_pools_on_main_thread;
/// Registration of default types to the `TypeRegistry` resource.
/// Registration of default types to the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource.
#[derive(Default)]
pub struct TypeRegistrationPlugin;
@ -95,10 +95,12 @@ fn register_math_types(app: &mut App) {
.register_type::<bevy_math::Mat3A>()
.register_type::<bevy_math::Mat4>()
.register_type::<bevy_math::DQuat>()
.register_type::<bevy_math::Quat>();
.register_type::<bevy_math::Quat>()
.register_type::<bevy_math::Rect>();
}
/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`, `IoTaskPool`.
/// Setup of default task pools: [`AsyncComputeTaskPool`](bevy_tasks::AsyncComputeTaskPool),
/// [`ComputeTaskPool`](bevy_tasks::ComputeTaskPool), [`IoTaskPool`](bevy_tasks::IoTaskPool).
#[derive(Default)]
pub struct TaskPoolPlugin {
/// Options for the [`TaskPool`](bevy_tasks::TaskPool) created at application start.

View file

@ -61,10 +61,10 @@ pub struct MotionVectorPrepass;
#[derive(Component)]
pub struct ViewPrepassTextures {
/// The depth texture generated by the prepass.
/// Exists only if [`DepthPrepass`] is added to the `ViewTarget`
/// Exists only if [`DepthPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
pub depth: Option<CachedTexture>,
/// The normals texture generated by the prepass.
/// Exists only if [`NormalPrepass`] is added to the `ViewTarget`
/// Exists only if [`NormalPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
pub normal: Option<CachedTexture>,
/// The motion vectors texture generated by the prepass.
/// Exists only if [`MotionVectorPrepass`] is added to the `ViewTarget`

View file

@ -148,7 +148,7 @@ impl Entity {
/// // ... replace the entities with valid ones.
/// ```
///
/// Deriving `Reflect` for a component that has an `Entity` field:
/// Deriving [`Reflect`](bevy_reflect::Reflect) for a component that has an `Entity` field:
///
/// ```no_run
/// # use bevy_ecs::{prelude::*, component::*};
@ -301,15 +301,15 @@ pub struct Entities {
/// that have been freed or are in the process of being allocated:
///
/// - The `freelist` IDs, previously freed by `free()`. These IDs are available to any of
/// `alloc()`, `reserve_entity()` or `reserve_entities()`. Allocation will always prefer
/// [`alloc`], [`reserve_entity`] or [`reserve_entities`]. Allocation will always prefer
/// these over brand new IDs.
///
/// - The `reserved` list of IDs that were once in the freelist, but got reserved by
/// `reserve_entities` or `reserve_entity()`. They are now waiting for `flush()` to make them
/// [`reserve_entities`] or [`reserve_entity`]. They are now waiting for [`flush`] to make them
/// fully allocated.
///
/// - The count of new IDs that do not yet exist in `self.meta`, but which we have handed out
/// and reserved. `flush()` will allocate room for them in `self.meta`.
/// and reserved. [`flush`] will allocate room for them in `self.meta`.
///
/// The contents of `pending` look like this:
///
@ -331,7 +331,12 @@ pub struct Entities {
/// This formulation allows us to reserve any number of IDs first from the freelist
/// and then from the new IDs, using only a single atomic subtract.
///
/// Once `flush()` is done, `free_cursor` will equal `pending.len()`.
/// Once [`flush`] is done, `free_cursor` will equal `pending.len()`.
///
/// [`alloc`]: Entities::alloc
/// [`reserve_entity`]: Entities::reserve_entity
/// [`reserve_entities`]: Entities::reserve_entities
/// [`flush`]: Entities::flush
pending: Vec<u32>,
free_cursor: AtomicIdCursor,
/// Stores the number of free entities for [`len`](Entities::len)
@ -350,7 +355,7 @@ impl Entities {
/// Reserve entity IDs concurrently.
///
/// Storage for entity generation and location is lazily allocated by calling `flush`.
/// Storage for entity generation and location is lazily allocated by calling [`flush`](Entities::flush).
pub fn reserve_entities(&self, count: u32) -> ReserveEntitiesIterator {
// Use one atomic subtract to grab a range of new IDs. The range might be
// entirely nonnegative, meaning all IDs come from the freelist, or entirely
@ -626,8 +631,8 @@ impl Entities {
*self.free_cursor.get_mut() != self.pending.len() as IdCursor
}
/// Allocates space for entities previously reserved with `reserve_entity` or
/// `reserve_entities`, then initializes each one using the supplied function.
/// Allocates space for entities previously reserved with [`reserve_entity`](Entities::reserve_entity) or
/// [`reserve_entities`](Entities::reserve_entities), then initializes each one using the supplied function.
///
/// # Safety
/// Flush _must_ set the entity location to the correct [`ArchetypeId`] for the given [`Entity`]

View file

@ -46,8 +46,8 @@ pub mod prelude {
system::{
adapter as system_adapter,
adapter::{dbg, error, ignore, info, unwrap, warn},
Commands, Deferred, In, IntoPipeSystem, IntoSystem, Local, NonSend, NonSendMut,
ParallelCommands, ParamSet, Query, Res, ResMut, Resource, System, SystemParamFunction,
Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands,
ParamSet, Query, Res, ResMut, Resource, System, SystemParamFunction,
},
world::{FromWorld, World},
};
@ -55,7 +55,7 @@ pub mod prelude {
pub use bevy_utils::all_tuples;
/// A specialized hashmap type with Key of `TypeId`
/// A specialized hashmap type with Key of [`TypeId`]
type TypeIdMap<V> = rustc_hash::FxHashMap<TypeId, V>;
#[cfg(test)]

View file

@ -25,6 +25,7 @@ struct FormattedBitSet<'a, T: SparseSetIndex> {
bit_set: &'a FixedBitSet,
_marker: PhantomData<T>,
}
impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> {
fn new(bit_set: &'a FixedBitSet) -> Self {
Self {
@ -33,6 +34,7 @@ impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> {
}
}
}
impl<'a, T: SparseSetIndex + fmt::Debug> fmt::Debug for FormattedBitSet<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
@ -69,6 +71,7 @@ impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for Access<T> {
.finish()
}
}
impl<T: SparseSetIndex> Default for Access<T> {
fn default() -> Self {
Self::new()
@ -144,7 +147,7 @@ impl<T: SparseSetIndex> Access<T> {
/// Returns `true` if the access and `other` can be active at the same time.
///
/// `Access` instances are incompatible if one can write
/// [`Access`] instances are incompatible if one can write
/// an element that the other can read or write.
pub fn is_compatible(&self, other: &Access<T>) -> bool {
// Only systems that do not write data are compatible with systems that operate on `&World`.
@ -213,31 +216,22 @@ impl<T: SparseSetIndex> Access<T> {
/// is read/write `T`, read `U`. It must still have a read `U` access otherwise the following
/// queries would be incorrectly considered disjoint:
/// - `Query<&mut T>` read/write `T`
/// - `Query<Option<&T>` accesses nothing
/// - `Query<Option<&T>>` accesses nothing
///
/// See comments the `WorldQuery` impls of `AnyOf`/`Option`/`Or` for more information.
#[derive(Clone, Eq, PartialEq)]
/// See comments the [`WorldQuery`](super::WorldQuery) impls of [`AnyOf`](super::AnyOf)/`Option`/[`Or`](super::Or) for more information.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct FilteredAccess<T: SparseSetIndex> {
access: Access<T>,
with: FixedBitSet,
without: FixedBitSet,
}
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for FilteredAccess<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FilteredAccess")
.field("access", &self.access)
.field("with", &FormattedBitSet::<T>::new(&self.with))
.field("without", &FormattedBitSet::<T>::new(&self.without))
.finish()
}
// An array of filter sets to express `With` or `Without` clauses in disjunctive normal form, for example: `Or<(With<A>, With<B>)>`.
// Filters like `(With<A>, Or<(With<B>, Without<C>)>` are expanded into `Or<((With<A>, With<B>), (With<A>, Without<C>))>`.
filter_sets: Vec<AccessFilters<T>>,
}
impl<T: SparseSetIndex> Default for FilteredAccess<T> {
fn default() -> Self {
Self {
access: Access::default(),
with: Default::default(),
without: Default::default(),
filter_sets: vec![AccessFilters::default()],
}
}
}
@ -266,30 +260,46 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
/// Adds access to the element given by `index`.
pub fn add_read(&mut self, index: T) {
self.access.add_read(index.clone());
self.add_with(index);
self.and_with(index);
}
/// Adds exclusive access to the element given by `index`.
pub fn add_write(&mut self, index: T) {
self.access.add_write(index.clone());
self.add_with(index);
self.and_with(index);
}
/// Retains only combinations where the element given by `index` is also present.
pub fn add_with(&mut self, index: T) {
self.with.grow(index.sparse_set_index() + 1);
self.with.insert(index.sparse_set_index());
/// Adds a `With` filter: corresponds to a conjunction (AND) operation.
///
/// Suppose we begin with `Or<(With<A>, With<B>)>`, which is represented by an array of two `AccessFilter` instances.
/// Adding `AND With<C>` via this method transforms it into the equivalent of `Or<((With<A>, With<C>), (With<B>, With<C>))>`.
pub fn and_with(&mut self, index: T) {
let index = index.sparse_set_index();
for filter in &mut self.filter_sets {
filter.with.grow(index + 1);
filter.with.insert(index);
}
}
/// Retains only combinations where the element given by `index` is not present.
pub fn add_without(&mut self, index: T) {
self.without.grow(index.sparse_set_index() + 1);
self.without.insert(index.sparse_set_index());
/// Adds a `Without` filter: corresponds to a conjunction (AND) operation.
///
/// Suppose we begin with `Or<(With<A>, With<B>)>`, which is represented by an array of two `AccessFilter` instances.
/// Adding `AND Without<C>` via this method transforms it into the equivalent of `Or<((With<A>, Without<C>), (With<B>, Without<C>))>`.
pub fn and_without(&mut self, index: T) {
let index = index.sparse_set_index();
for filter in &mut self.filter_sets {
filter.without.grow(index + 1);
filter.without.insert(index);
}
}
pub fn extend_intersect_filter(&mut self, other: &FilteredAccess<T>) {
self.without.intersect_with(&other.without);
self.with.intersect_with(&other.with);
/// Appends an array of filters: corresponds to a disjunction (OR) operation.
///
/// As the underlying array of filters represents a disjunction,
/// where each element (`AccessFilters`) represents a conjunction,
/// we can simply append to the array.
pub fn append_or(&mut self, other: &FilteredAccess<T>) {
self.filter_sets.append(&mut other.filter_sets.clone());
}
pub fn extend_access(&mut self, other: &FilteredAccess<T>) {
@ -298,9 +308,23 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
/// Returns `true` if this and `other` can be active at the same time.
pub fn is_compatible(&self, other: &FilteredAccess<T>) -> bool {
self.access.is_compatible(&other.access)
|| !self.with.is_disjoint(&other.without)
|| !other.with.is_disjoint(&self.without)
if self.access.is_compatible(&other.access) {
return true;
}
// If the access instances are incompatible, we want to check that whether filters can
// guarantee that queries are disjoint.
// Since the `filter_sets` array represents a Disjunctive Normal Form formula ("ORs of ANDs"),
// we need to make sure that each filter set (ANDs) rule out every filter set from the `other` instance.
//
// For example, `Query<&mut C, Or<(With<A>, Without<B>)>>` is compatible `Query<&mut C, (With<B>, Without<A>)>`,
// but `Query<&mut C, Or<(Without<A>, Without<B>)>>` isn't compatible with `Query<&mut C, Or<(With<A>, With<B>)>>`.
self.filter_sets.iter().all(|filter| {
other
.filter_sets
.iter()
.all(|other_filter| filter.is_ruled_out_by(other_filter))
})
}
/// Returns a vector of elements that this and `other` cannot access at the same time.
@ -313,10 +337,34 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
}
/// Adds all access and filters from `other`.
pub fn extend(&mut self, access: &FilteredAccess<T>) {
self.access.extend(&access.access);
self.with.union_with(&access.with);
self.without.union_with(&access.without);
///
/// Corresponds to a conjunction operation (AND) for filters.
///
/// Extending `Or<(With<A>, Without<B>)>` with `Or<(With<C>, Without<D>)>` will result in
/// `Or<((With<A>, With<C>), (With<A>, Without<D>), (Without<B>, With<C>), (Without<B>, Without<D>))>`.
pub fn extend(&mut self, other: &FilteredAccess<T>) {
self.access.extend(&other.access);
// We can avoid allocating a new array of bitsets if `other` contains just a single set of filters:
// in this case we can short-circuit by performing an in-place union for each bitset.
if other.filter_sets.len() == 1 {
for filter in &mut self.filter_sets {
filter.with.union_with(&other.filter_sets[0].with);
filter.without.union_with(&other.filter_sets[0].without);
}
return;
}
let mut new_filters = Vec::with_capacity(self.filter_sets.len() * other.filter_sets.len());
for filter in &self.filter_sets {
for other_filter in &other.filter_sets {
let mut new_filter = filter.clone();
new_filter.with.union_with(&other_filter.with);
new_filter.without.union_with(&other_filter.without);
new_filters.push(new_filter);
}
}
self.filter_sets = new_filters;
}
/// Sets the underlying unfiltered access as having access to all indexed elements.
@ -325,6 +373,43 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
}
}
#[derive(Clone, Eq, PartialEq)]
struct AccessFilters<T> {
with: FixedBitSet,
without: FixedBitSet,
_index_type: PhantomData<T>,
}
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for AccessFilters<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AccessFilters")
.field("with", &FormattedBitSet::<T>::new(&self.with))
.field("without", &FormattedBitSet::<T>::new(&self.without))
.finish()
}
}
impl<T: SparseSetIndex> Default for AccessFilters<T> {
fn default() -> Self {
Self {
with: FixedBitSet::default(),
without: FixedBitSet::default(),
_index_type: PhantomData,
}
}
}
impl<T: SparseSetIndex> AccessFilters<T> {
fn is_ruled_out_by(&self, other: &Self) -> bool {
// Although not technically complete, we don't consider the case when `AccessFilters`'s
// `without` bitset contradicts its own `with` bitset (e.g. `(With<A>, Without<A>)`).
// Such query would be considered compatible with any other query, but as it's almost
// always an error, we ignore this case instead of treating such query as compatible
// with others.
!self.with.is_disjoint(&other.without) || !self.without.is_disjoint(&other.with)
}
}
/// A collection of [`FilteredAccess`] instances.
///
/// Used internally to statically check if systems have conflicting access.
@ -353,7 +438,7 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
/// compatible.
/// 2. A "fine grained" check, it kicks in when the "coarse" check fails.
/// the two access sets might still be compatible if some of the accesses
/// are restricted with the `With` or `Without` filters so that access is
/// are restricted with the [`With`](super::With) or [`Without`](super::Without) filters so that access is
/// mutually exclusive. The fine grained phase iterates over all filters in
/// the `self` set and compares it to all the filters in the `other` set,
/// making sure they are all mutually compatible.
@ -441,7 +526,10 @@ impl<T: SparseSetIndex> Default for FilteredAccessSet<T> {
#[cfg(test)]
mod tests {
use crate::query::access::AccessFilters;
use crate::query::{Access, FilteredAccess, FilteredAccessSet};
use fixedbitset::FixedBitSet;
use std::marker::PhantomData;
#[test]
fn read_all_access_conflicts() {
@ -514,22 +602,67 @@ mod tests {
let mut access_a = FilteredAccess::<usize>::default();
access_a.add_read(0);
access_a.add_read(1);
access_a.add_with(2);
access_a.and_with(2);
let mut access_b = FilteredAccess::<usize>::default();
access_b.add_read(0);
access_b.add_write(3);
access_b.add_without(4);
access_b.and_without(4);
access_a.extend(&access_b);
let mut expected = FilteredAccess::<usize>::default();
expected.add_read(0);
expected.add_read(1);
expected.add_with(2);
expected.and_with(2);
expected.add_write(3);
expected.add_without(4);
expected.and_without(4);
assert!(access_a.eq(&expected));
}
#[test]
fn filtered_access_extend_or() {
let mut access_a = FilteredAccess::<usize>::default();
// Exclusive access to `(&mut A, &mut B)`.
access_a.add_write(0);
access_a.add_write(1);
// Filter by `With<C>`.
let mut access_b = FilteredAccess::<usize>::default();
access_b.and_with(2);
// Filter by `(With<D>, Without<E>)`.
let mut access_c = FilteredAccess::<usize>::default();
access_c.and_with(3);
access_c.and_without(4);
// Turns `access_b` into `Or<(With<C>, (With<D>, Without<D>))>`.
access_b.append_or(&access_c);
// Applies the filters to the initial query, which corresponds to the FilteredAccess'
// representation of `Query<(&mut A, &mut B), Or<(With<C>, (With<D>, Without<E>))>>`.
access_a.extend(&access_b);
// Construct the expected `FilteredAccess` struct.
// The intention here is to test that exclusive access implied by `add_write`
// forms correct normalized access structs when extended with `Or` filters.
let mut expected = FilteredAccess::<usize>::default();
expected.add_write(0);
expected.add_write(1);
// The resulted access is expected to represent `Or<((With<A>, With<B>, With<C>), (With<A>, With<B>, With<D>, Without<E>))>`.
expected.filter_sets = vec![
AccessFilters {
with: FixedBitSet::with_capacity_and_blocks(3, [0b111]),
without: FixedBitSet::default(),
_index_type: PhantomData,
},
AccessFilters {
with: FixedBitSet::with_capacity_and_blocks(4, [0b1011]),
without: FixedBitSet::with_capacity_and_blocks(5, [0b10000]),
_index_type: PhantomData,
},
];
assert_eq!(access_a, expected);
}
}

View file

@ -1157,8 +1157,8 @@ macro_rules! impl_tuple_fetch {
}
#[inline(always)]
unsafe fn filter_fetch<'w>(
_fetch: &mut Self::Fetch<'w>,
unsafe fn filter_fetch(
_fetch: &mut Self::Fetch<'_>,
_entity: Entity,
_table_row: TableRow
) -> bool {
@ -1281,34 +1281,21 @@ macro_rules! impl_anytuple_fetch {
fn update_component_access(state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {
let ($($name,)*) = state;
// We do not unconditionally add `$name`'s `with`/`without` accesses to `_access`
// as this would be unsound. For example the following two queries should conflict:
// - Query<(AnyOf<(&A, ())>, &mut B)>
// - Query<&mut B, Without<A>>
//
// If we were to unconditionally add `$name`'s `with`/`without` accesses then `AnyOf<(&A, ())>`
// would have a `With<A>` access which is incorrect as this `WorldQuery` will match entities that
// do not have the `A` component. This is the same logic as the `Or<...>: WorldQuery` impl.
//
// The correct thing to do here is to only add a `with`/`without` access to `_access` if all
// `$name` params have that `with`/`without` access. More jargony put- we add the intersection
// of all `with`/`without` accesses of the `$name` params to `_access`.
let mut _intersected_access = _access.clone();
let mut _new_access = _access.clone();
let mut _not_first = false;
$(
if _not_first {
let mut intermediate = _access.clone();
$name::update_component_access($name, &mut intermediate);
_intersected_access.extend_intersect_filter(&intermediate);
_intersected_access.extend_access(&intermediate);
_new_access.append_or(&intermediate);
_new_access.extend_access(&intermediate);
} else {
$name::update_component_access($name, &mut _intersected_access);
$name::update_component_access($name, &mut _new_access);
_not_first = true;
}
)*
*_access = _intersected_access;
*_access = _new_access;
}
fn update_archetype_component_access(state: &Self::State, _archetype: &Archetype, _access: &mut Access<ArchetypeComponentId>) {

View file

@ -86,7 +86,7 @@ unsafe impl<T: Component> WorldQuery for With<T> {
#[inline]
fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess<ComponentId>) {
access.add_with(id);
access.and_with(id);
}
#[inline]
@ -183,7 +183,7 @@ unsafe impl<T: Component> WorldQuery for Without<T> {
#[inline]
fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess<ComponentId>) {
access.add_without(id);
access.and_without(id);
}
#[inline]
@ -328,8 +328,8 @@ macro_rules! impl_query_filter_tuple {
}
#[inline(always)]
unsafe fn filter_fetch<'w>(
fetch: &mut Self::Fetch<'w>,
unsafe fn filter_fetch(
fetch: &mut Self::Fetch<'_>,
entity: Entity,
table_row: TableRow
) -> bool {
@ -339,33 +339,21 @@ macro_rules! impl_query_filter_tuple {
fn update_component_access(state: &Self::State, access: &mut FilteredAccess<ComponentId>) {
let ($($filter,)*) = state;
// We do not unconditionally add `$filter`'s `with`/`without` accesses to `access`
// as this would be unsound. For example the following two queries should conflict:
// - Query<&mut B, Or<(With<A>, ())>>
// - Query<&mut B, Without<A>>
//
// If we were to unconditionally add `$name`'s `with`/`without` accesses then `Or<(With<A>, ())>`
// would have a `With<A>` access which is incorrect as this `WorldQuery` will match entities that
// do not have the `A` component. This is the same logic as the `AnyOf<...>: WorldQuery` impl.
//
// The correct thing to do here is to only add a `with`/`without` access to `_access` if all
// `$filter` params have that `with`/`without` access. More jargony put- we add the intersection
// of all `with`/`without` accesses of the `$filter` params to `access`.
let mut _intersected_access = access.clone();
let mut _new_access = access.clone();
let mut _not_first = false;
$(
if _not_first {
let mut intermediate = access.clone();
$filter::update_component_access($filter, &mut intermediate);
_intersected_access.extend_intersect_filter(&intermediate);
_intersected_access.extend_access(&intermediate);
_new_access.append_or(&intermediate);
_new_access.extend_access(&intermediate);
} else {
$filter::update_component_access($filter, &mut _intersected_access);
$filter::update_component_access($filter, &mut _new_access);
_not_first = true;
}
)*
*access = _intersected_access;
*access = _new_access;
}
fn update_archetype_component_access(state: &Self::State, archetype: &Archetype, access: &mut Access<ArchetypeComponentId>) {
@ -516,8 +504,8 @@ macro_rules! impl_tick_filter {
}
#[inline(always)]
unsafe fn filter_fetch<'w>(
fetch: &mut Self::Fetch<'w>,
unsafe fn filter_fetch(
fetch: &mut Self::Fetch<'_>,
entity: Entity,
table_row: TableRow
) -> bool {

View file

@ -143,6 +143,7 @@ pub mod common_conditions {
change_detection::DetectChanges,
event::{Event, EventReader},
prelude::{Component, Query, With},
removal_detection::RemovedComponents,
schedule::{State, States},
system::{IntoSystem, Res, Resource, System},
};
@ -893,6 +894,17 @@ pub mod common_conditions {
move |query: Query<(), With<T>>| !query.is_empty()
}
/// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true`
/// if there are any entity with a component of the given type removed.
pub fn any_component_removed<T: Component>() -> impl FnMut(RemovedComponents<T>) -> bool {
// `RemovedComponents` based on events and therefore events need to be consumed,
// so that there are no false positives on subsequent calls of the run condition.
// Simply checking `is_empty` would not be enough.
// PERF: note that `count` is efficient (not actually looping/iterating),
// due to Bevy having a specialized implementation for events.
move |mut removals: RemovedComponents<T>| !removals.iter().count() != 0
}
/// Generates a [`Condition`](super::Condition) that inverses the result of passed one.
///
/// # Example

View file

@ -47,7 +47,7 @@ pub enum ExecutorKind {
/// (along with dependency information for multi-threaded execution).
///
/// Since the arrays are sorted in the same order, elements are referenced by their index.
/// `FixedBitSet` is used as a smaller, more efficient substitute of `HashSet<usize>`.
/// [`FixedBitSet`] is used as a smaller, more efficient substitute of `HashSet<usize>`.
#[derive(Default)]
pub struct SystemSchedule {
pub(super) systems: Vec<BoxedSystem>,

View file

@ -56,7 +56,7 @@ impl SyncUnsafeSchedule<'_> {
/// Per-system data used by the [`MultiThreadedExecutor`].
// Copied here because it can't be read from the system when it's running.
struct SystemTaskMetadata {
/// The `ArchetypeComponentId` access of the system.
/// The [`ArchetypeComponentId`] access of the system.
archetype_component_access: Access<ArchetypeComponentId>,
/// Indices of the systems that directly depend on the system.
dependents: Vec<usize>,
@ -320,6 +320,8 @@ impl MultiThreadedExecutor {
self.ready_systems.set(system_index, false);
// SAFETY: Since `self.can_run` returned true earlier, it must have called
// `update_archetype_component_access` for each run condition.
if !self.should_run(system_index, system, conditions, world) {
self.skip_system_and_signal_dependents(system_index);
continue;
@ -338,7 +340,9 @@ impl MultiThreadedExecutor {
break;
}
// SAFETY: No other reference to this system exists.
// SAFETY:
// - No other reference to this system exists.
// - `self.can_run` has been called, which calls `update_archetype_component_access` with this system.
unsafe {
self.spawn_system_task(scope, system_index, systems, world);
}
@ -408,7 +412,11 @@ impl MultiThreadedExecutor {
true
}
fn should_run(
/// # Safety
///
/// `update_archetype_component` must have been called with `world`
/// for each run condition in `conditions`.
unsafe fn should_run(
&mut self,
system_index: usize,
_system: &BoxedSystem,
@ -421,7 +429,8 @@ impl MultiThreadedExecutor {
continue;
}
// evaluate system set's conditions
// Evaluate the system set's conditions.
// SAFETY: `update_archetype_component_access` has been called for each run condition.
let set_conditions_met =
evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world);
@ -434,7 +443,8 @@ impl MultiThreadedExecutor {
self.evaluated_sets.insert(set_idx);
}
// evaluate system's conditions
// Evaluate the system's conditions.
// SAFETY: `update_archetype_component_access` has been called for each run condition.
let system_conditions_met =
evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world);
@ -448,7 +458,9 @@ impl MultiThreadedExecutor {
}
/// # Safety
/// Caller must not alias systems that are running.
/// - Caller must not alias systems that are running.
/// - `update_archetype_component_access` must have been called with `world`
/// on the system assocaited with `system_index`.
unsafe fn spawn_system_task<'scope>(
&mut self,
scope: &Scope<'_, 'scope, ()>,
@ -470,7 +482,9 @@ impl MultiThreadedExecutor {
#[cfg(feature = "trace")]
let system_guard = system_span.enter();
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
// SAFETY: access is compatible
// SAFETY:
// - Access: TODO.
// - `update_archetype_component_access` has been called.
unsafe { system.run_unsafe((), world) };
}));
#[cfg(feature = "trace")]
@ -673,7 +687,11 @@ fn apply_system_buffers(
Ok(())
}
fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &World) -> bool {
/// # Safety
///
/// `update_archetype_component_access` must have been called
/// with `world` for each condition in `conditions`.
unsafe fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &World) -> bool {
// not short-circuiting is intentional
#[allow(clippy::unnecessary_fold)]
conditions

View file

@ -172,3 +172,40 @@ where
SystemTypeSet::new()
}
}
#[cfg(test)]
mod tests {
use crate::{
schedule::{tests::ResMut, Schedule},
system::Resource,
};
use super::*;
#[test]
fn test_boxed_label() {
use crate::{self as bevy_ecs, world::World};
#[derive(Resource)]
struct Flag(bool);
#[derive(ScheduleLabel, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
struct A;
let mut world = World::new();
let mut schedule = Schedule::new();
schedule.add_systems(|mut flag: ResMut<Flag>| flag.0 = true);
world.add_schedule(schedule, A);
let boxed: Box<dyn ScheduleLabel> = Box::new(A);
world.insert_resource(Flag(false));
world.run_schedule(&boxed);
assert!(world.resource::<Flag>().0);
world.insert_resource(Flag(false));
world.run_schedule(boxed);
assert!(world.resource::<Flag>().0);
}
}

View file

@ -11,7 +11,7 @@ use bevy_utils::OnDrop;
/// A flat, type-erased data storage type
///
/// Used to densely store homogeneous ECS data. A blob is usually just an arbitrary block of contiguous memory without any identity, and
/// could be used to represent any arbitrary data (i.e. string, arrays, etc). This type is an extendable and reallcatable blob, which makes
/// could be used to represent any arbitrary data (i.e. string, arrays, etc). This type is an extendable and re-allocatable blob, which makes
/// it a blobby Vec, a `BlobVec`.
pub(super) struct BlobVec {
item_layout: Layout,

View file

@ -259,7 +259,7 @@ impl<const SEND: bool> Resources<SEND> {
///
/// # Panics
/// Will panic if `component_id` is not valid for the provided `components`
/// If `SEND` is false, this will panic if `component_id`'s `ComponentInfo` is not registered as being `Send` + `Sync`.
/// If `SEND` is true, this will panic if `component_id`'s `ComponentInfo` is not registered as being `Send` + `Sync`.
pub(crate) fn initialize_with(
&mut self,
component_id: ComponentId,
@ -269,7 +269,11 @@ impl<const SEND: bool> Resources<SEND> {
self.resources.get_or_insert_with(component_id, || {
let component_info = components.get_info(component_id).unwrap();
if SEND {
assert!(component_info.is_send_and_sync());
assert!(
component_info.is_send_and_sync(),
"Send + Sync resource {} initialized as non_send. It may have been inserted via World::insert_non_send_resource by accident. Try using World::insert_resource instead.",
component_info.name(),
);
}
ResourceData {
column: ManuallyDrop::new(Column::with_capacity(component_info, 1)),

View file

@ -722,7 +722,7 @@ mod tests {
);
fn init_component<T: Component>(sets: &mut SparseSets, id: usize) {
let descriptor = ComponentDescriptor::new::<TestComponent1>();
let descriptor = ComponentDescriptor::new::<T>();
let id = ComponentId::new(id);
let info = ComponentInfo::new(id, descriptor);
sets.get_or_insert(&info);

View file

@ -164,6 +164,8 @@ where
// so the caller will guarantee that no other systems will conflict with `a` or `b`.
// Since these closures are `!Send + !Sync + !'static`, they can never be called
// in parallel, so their world accesses will not conflict with each other.
// Additionally, `update_archetype_component_access` has been called,
// which forwards to the implementations for `self.a` and `self.b`.
|input| self.a.run_unsafe(input, world),
|input| self.b.run_unsafe(input, world),
)
@ -235,3 +237,65 @@ where
B: ReadOnlySystem,
{
}
/// A [`System`] created by piping the output of the first system into the input of the second.
///
/// This can be repeated indefinitely, but system pipes cannot branch: the output is consumed by the receiving system.
///
/// Given two systems `A` and `B`, A may be piped into `B` as `A.pipe(B)` if the output type of `A` is
/// equal to the input type of `B`.
///
/// Note that for [`FunctionSystem`](crate::system::FunctionSystem)s the output is the return value
/// of the function and the input is the first [`SystemParam`](crate::system::SystemParam) if it is
/// tagged with [`In`](crate::system::In) or `()` if the function has no designated input parameter.
///
/// # Examples
///
/// ```
/// use std::num::ParseIntError;
///
/// use bevy_ecs::prelude::*;
///
/// fn main() {
/// let mut world = World::default();
/// world.insert_resource(Message("42".to_string()));
///
/// // pipe the `parse_message_system`'s output into the `filter_system`s input
/// let mut piped_system = parse_message_system.pipe(filter_system);
/// piped_system.initialize(&mut world);
/// assert_eq!(piped_system.run((), &mut world), Some(42));
/// }
///
/// #[derive(Resource)]
/// struct Message(String);
///
/// fn parse_message_system(message: Res<Message>) -> Result<usize, ParseIntError> {
/// message.0.parse::<usize>()
/// }
///
/// fn filter_system(In(result): In<Result<usize, ParseIntError>>) -> Option<usize> {
/// result.ok().filter(|&n| n < 100)
/// }
/// ```
pub type PipeSystem<SystemA, SystemB> = CombinatorSystem<Pipe, SystemA, SystemB>;
#[doc(hidden)]
pub struct Pipe;
impl<A, B> Combine<A, B> for Pipe
where
A: System,
B: System<In = A::Out>,
{
type In = A::In;
type Out = B::Out;
fn combine(
input: Self::In,
a: impl FnOnce(A::In) -> A::Out,
b: impl FnOnce(B::In) -> B::Out,
) -> Self::Out {
let value = a(input);
b(value)
}
}

View file

@ -184,7 +184,7 @@ impl<'w, 's> Commands<'w, 's> {
/// Pushes a [`Command`] to the queue for creating a new [`Entity`] if the given one does not exists,
/// and returns its corresponding [`EntityCommands`].
///
/// This method silently fails by returning `EntityCommands`
/// This method silently fails by returning [`EntityCommands`]
/// even if the given `Entity` cannot be spawned.
///
/// See [`World::get_or_spawn`] for more details.
@ -345,7 +345,7 @@ impl<'w, 's> Commands<'w, 's> {
/// Pushes a [`Command`] to the queue for creating entities with a particular [`Bundle`] type.
///
/// `bundles_iter` is a type that can be converted into a `Bundle` iterator
/// `bundles_iter` is a type that can be converted into a [`Bundle`] iterator
/// (it can also be a collection).
///
/// This method is equivalent to iterating `bundles_iter`

View file

@ -6,7 +6,7 @@ use crate::{
check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, In, IntoSystem,
System, SystemMeta,
},
world::{World, WorldId},
world::World,
};
use bevy_utils::all_tuples;
@ -25,7 +25,6 @@ where
func: F,
param_state: Option<<F::Param as ExclusiveSystemParam>::State>,
system_meta: SystemMeta,
world_id: Option<WorldId>,
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
marker: PhantomData<fn() -> Marker>,
}
@ -43,7 +42,6 @@ where
func,
param_state: None,
system_meta: SystemMeta::new::<F>(),
world_id: None,
marker: PhantomData,
}
}
@ -132,7 +130,6 @@ where
#[inline]
fn initialize(&mut self, world: &mut World) {
self.world_id = Some(world.id());
self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX);
self.param_state = Some(F::Param::init(world, &mut self.system_meta));
}

View file

@ -10,7 +10,7 @@ use crate::{
use bevy_utils::all_tuples;
use std::{any::TypeId, borrow::Cow, marker::PhantomData};
use super::ReadOnlySystem;
use super::{In, IntoSystem, ReadOnlySystem};
/// The metadata of a [`System`].
#[derive(Clone)]
@ -308,65 +308,6 @@ impl<Param: SystemParam> FromWorld for SystemState<Param> {
}
}
/// Conversion trait to turn something into a [`System`].
///
/// Use this to get a system from a function. Also note that every system implements this trait as
/// well.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// fn my_system_function(a_usize_local: Local<usize>) {}
///
/// let system = IntoSystem::into_system(my_system_function);
/// ```
// This trait has to be generic because we have potentially overlapping impls, in particular
// because Rust thinks a type could impl multiple different `FnMut` combinations
// even though none can currently
pub trait IntoSystem<In, Out, Marker>: Sized {
type System: System<In = In, Out = Out>;
/// Turns this value into its corresponding [`System`].
fn into_system(this: Self) -> Self::System;
}
// Systems implicitly implement IntoSystem
impl<In, Out, Sys: System<In = In, Out = Out>> IntoSystem<In, Out, ()> for Sys {
type System = Sys;
fn into_system(this: Self) -> Sys {
this
}
}
/// Wrapper type to mark a [`SystemParam`] as an input.
///
/// [`System`]s may take an optional input which they require to be passed to them when they
/// are being [`run`](System::run). For [`FunctionSystems`](FunctionSystem) the input may be marked
/// with this `In` type, but only the first param of a function may be tagged as an input. This also
/// means a system can only have one or zero input parameters.
///
/// # Examples
///
/// Here is a simple example of a system that takes a [`usize`] returning the square of it.
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// fn main() {
/// let mut square_system = IntoSystem::into_system(square);
///
/// let mut world = World::default();
/// square_system.initialize(&mut world);
/// assert_eq!(square_system.run(12, &mut world), 144);
/// }
///
/// fn square(In(input): In<usize>) -> usize {
/// input * input
/// }
/// ```
pub struct In<In>(pub In);
/// The [`System`] counter part of an ordinary function.
///
/// You get this by calling [`IntoSystem::into_system`] on a function that only accepts
@ -479,10 +420,11 @@ where
unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out {
let change_tick = world.increment_change_tick();
// Safety:
// We update the archetype component access correctly based on `Param`'s requirements
// in `update_archetype_component_access`.
// Our caller upholds the requirements.
// SAFETY:
// - The caller has invoked `update_archetype_component_access`, which will panic
// if the world does not match.
// - All world accesses used by `F::Param` have been registered, so the caller
// will ensure that there are no data access conflicts.
let params = F::Param::get_param(
self.param_state.as_mut().expect(Self::PARAM_MESSAGE),
&self.system_meta,
@ -547,7 +489,7 @@ where
}
}
/// SAFETY: `F`'s param is `ReadOnlySystemParam`, so this system will only read from the world.
/// SAFETY: `F`'s param is [`ReadOnlySystemParam`], so this system will only read from the world.
unsafe impl<Marker, F> ReadOnlySystem for FunctionSystem<Marker, F>
where
Marker: 'static,

View file

@ -111,7 +111,8 @@ mod query;
#[allow(clippy::module_inception)]
mod system;
mod system_param;
mod system_piping;
use std::borrow::Cow;
pub use combinator::*;
pub use commands::*;
@ -121,7 +122,6 @@ pub use function_system::*;
pub use query::*;
pub use system::*;
pub use system_param::*;
pub use system_piping::*;
use crate::world::World;
@ -190,10 +190,297 @@ where
assert_is_system(system);
}
/// Conversion trait to turn something into a [`System`].
///
/// Use this to get a system from a function. Also note that every system implements this trait as
/// well.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// fn my_system_function(a_usize_local: Local<usize>) {}
///
/// let system = IntoSystem::into_system(my_system_function);
/// ```
// This trait has to be generic because we have potentially overlapping impls, in particular
// because Rust thinks a type could impl multiple different `FnMut` combinations
// even though none can currently
pub trait IntoSystem<In, Out, Marker>: Sized {
type System: System<In = In, Out = Out>;
/// Turns this value into its corresponding [`System`].
fn into_system(this: Self) -> Self::System;
/// Pass the output of this system `A` into a second system `B`, creating a new compound system.
///
/// The second system must have `In<T>` as its first parameter, where `T`
/// is the return type of the first system.
fn pipe<B, Final, MarkerB>(self, system: B) -> PipeSystem<Self::System, B::System>
where
B: IntoSystem<Out, Final, MarkerB>,
{
let system_a = IntoSystem::into_system(self);
let system_b = IntoSystem::into_system(system);
let name = format!("Pipe({}, {})", system_a.name(), system_b.name());
PipeSystem::new(system_a, system_b, Cow::Owned(name))
}
}
// All systems implicitly implement IntoSystem.
impl<T: System> IntoSystem<T::In, T::Out, ()> for T {
type System = T;
fn into_system(this: Self) -> Self {
this
}
}
/// Wrapper type to mark a [`SystemParam`] as an input.
///
/// [`System`]s may take an optional input which they require to be passed to them when they
/// are being [`run`](System::run). For [`FunctionSystems`](FunctionSystem) the input may be marked
/// with this `In` type, but only the first param of a function may be tagged as an input. This also
/// means a system can only have one or zero input parameters.
///
/// # Examples
///
/// Here is a simple example of a system that takes a [`usize`] returning the square of it.
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// fn main() {
/// let mut square_system = IntoSystem::into_system(square);
///
/// let mut world = World::default();
/// square_system.initialize(&mut world);
/// assert_eq!(square_system.run(12, &mut world), 144);
/// }
///
/// fn square(In(input): In<usize>) -> usize {
/// input * input
/// }
/// ```
pub struct In<In>(pub In);
/// A collection of common adapters for [piping](crate::system::PipeSystem) the result of a system.
pub mod adapter {
use crate::system::In;
use bevy_utils::tracing;
use std::fmt::Debug;
/// Converts a regular function into a system adapter.
///
/// # Examples
/// ```
/// use bevy_ecs::prelude::*;
///
/// fn return1() -> u64 { 1 }
///
/// return1
/// .pipe(system_adapter::new(u32::try_from))
/// .pipe(system_adapter::unwrap)
/// .pipe(print);
///
/// fn print(In(x): In<impl std::fmt::Debug>) {
/// println!("{x:?}");
/// }
/// ```
pub fn new<T, U>(mut f: impl FnMut(T) -> U) -> impl FnMut(In<T>) -> U {
move |In(x)| f(x)
}
/// System adapter that unwraps the `Ok` variant of a [`Result`].
/// This is useful for fallible systems that should panic in the case of an error.
///
/// There is no equivalent adapter for [`Option`]. Instead, it's best to provide
/// an error message and convert to a `Result` using `ok_or{_else}`.
///
/// # Examples
///
/// Panicking on error
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Panic if the load system returns an error.
/// load_save_system.pipe(system_adapter::unwrap)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system which may fail irreparably.
/// fn load_save_system() -> Result<(), std::io::Error> {
/// let save_file = open_file("my_save.json")?;
/// dbg!(save_file);
/// Ok(())
/// }
/// # fn open_file(name: &str) -> Result<&'static str, std::io::Error>
/// # { Ok("hello world") }
/// ```
pub fn unwrap<T, E: Debug>(In(res): In<Result<T, E>>) -> T {
res.unwrap()
}
/// System adapter that utilizes the [`bevy_utils::tracing::info!`] macro to print system information.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints system information.
/// data_pipe_system.pipe(system_adapter::info)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a String output.
/// fn data_pipe_system() -> String {
/// "42".to_string()
/// }
/// ```
pub fn info<T: Debug>(In(data): In<T>) {
tracing::info!("{:?}", data);
}
/// System adapter that utilizes the [`bevy_utils::tracing::debug!`] macro to print the output of a system.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints debug data from system.
/// parse_message_system.pipe(system_adapter::dbg)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a Result<usize, String> output.
/// fn parse_message_system() -> Result<usize, std::num::ParseIntError> {
/// Ok("42".parse()?)
/// }
/// ```
pub fn dbg<T: Debug>(In(data): In<T>) {
tracing::debug!("{:?}", data);
}
/// System adapter that utilizes the [`bevy_utils::tracing::warn!`] macro to print the output of a system.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// # let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints system warning if system returns an error.
/// warning_pipe_system.pipe(system_adapter::warn)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a Result<(), String> output.
/// fn warning_pipe_system() -> Result<(), String> {
/// Err("Got to rusty?".to_string())
/// }
/// ```
pub fn warn<E: Debug>(In(res): In<Result<(), E>>) {
if let Err(warn) = res {
tracing::warn!("{:?}", warn);
}
}
/// System adapter that utilizes the [`bevy_utils::tracing::error!`] macro to print the output of a system.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints system error if system fails.
/// parse_error_message_system.pipe(system_adapter::error)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a Result<())> output.
/// fn parse_error_message_system() -> Result<(), String> {
/// Err("Some error".to_owned())
/// }
/// ```
pub fn error<E: Debug>(In(res): In<Result<(), E>>) {
if let Err(error) = res {
tracing::error!("{:?}", error);
}
}
/// System adapter that ignores the output of the previous system in a pipe.
/// This is useful for fallible systems that should simply return early in case of an `Err`/`None`.
///
/// # Examples
///
/// Returning early
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Marker component for an enemy entity.
/// #[derive(Component)]
/// struct Monster;
///
/// // Building a new schedule/app...
/// # let mut sched = Schedule::default(); sched
/// .add_systems(
/// // If the system fails, just move on and try again next frame.
/// fallible_system.pipe(system_adapter::ignore)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system which may return early. It's more convenient to use the `?` operator for this.
/// fn fallible_system(
/// q: Query<Entity, With<Monster>>
/// ) -> Option<()> {
/// let monster_id = q.iter().next()?;
/// println!("Monster entity is {monster_id:?}");
/// Some(())
/// }
/// ```
pub fn ignore<T>(In(_): In<T>) {}
}
#[cfg(test)]
mod tests {
use std::any::TypeId;
use bevy_utils::default;
use crate::{
self as bevy_ecs,
archetype::{ArchetypeComponentId, Archetypes},
@ -206,8 +493,8 @@ mod tests {
removal_detection::RemovedComponents,
schedule::{apply_system_buffers, IntoSystemConfigs, Schedule},
system::{
Commands, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, QueryComponentError,
Res, ResMut, Resource, System, SystemState,
adapter::new, Commands, In, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query,
QueryComponentError, Res, ResMut, Resource, System, SystemState,
},
world::{FromWorld, World},
};
@ -483,6 +770,13 @@ mod tests {
run_system(&mut world, sys);
}
#[test]
fn any_of_and_without() {
fn sys(_: Query<(AnyOf<(&A, &B)>, &mut C)>, _: Query<&mut C, (Without<A>, Without<B>)>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "error[B0001]"]
fn or_has_no_filter_with() {
@ -498,6 +792,113 @@ mod tests {
run_system(&mut world, sys);
}
#[test]
fn or_has_filter_with() {
fn sys(
_: Query<&mut C, Or<(With<A>, With<B>)>>,
_: Query<&mut C, (Without<A>, Without<B>)>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn or_expanded_with_and_without_common() {
fn sys(_: Query<&mut D, (With<A>, Or<(With<B>, With<C>)>)>, _: Query<&mut D, Without<A>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn or_expanded_nested_with_and_without_common() {
fn sys(
_: Query<&mut E, (Or<((With<B>, With<C>), (With<C>, With<D>))>, With<A>)>,
_: Query<&mut E, (Without<B>, Without<D>)>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "error[B0001]"]
fn or_expanded_nested_with_and_disjoint_without() {
fn sys(
_: Query<&mut E, (Or<((With<B>, With<C>), (With<C>, With<D>))>, With<A>)>,
_: Query<&mut E, Without<D>>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "error[B0001]"]
fn or_expanded_nested_or_with_and_disjoint_without() {
fn sys(
_: Query<&mut D, Or<(Or<(With<A>, With<B>)>, Or<(With<A>, With<C>)>)>>,
_: Query<&mut D, Without<A>>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn or_expanded_nested_with_and_common_nested_without() {
fn sys(
_: Query<&mut D, Or<((With<A>, With<B>), (With<B>, With<C>))>>,
_: Query<&mut D, Or<(Without<D>, Without<B>)>>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn or_with_without_and_compatible_with_without() {
fn sys(
_: Query<&mut C, Or<(With<A>, Without<B>)>>,
_: Query<&mut C, (With<B>, Without<A>)>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "error[B0001]"]
fn with_and_disjoint_or_empty_without() {
fn sys(_: Query<&mut B, With<A>>, _: Query<&mut B, Or<((), Without<A>)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "error[B0001]"]
fn or_expanded_with_and_disjoint_nested_without() {
fn sys(
_: Query<&mut D, Or<(With<A>, With<B>)>>,
_: Query<&mut D, Or<(Without<A>, Without<B>)>>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "error[B0001]"]
fn or_expanded_nested_with_and_disjoint_nested_without() {
fn sys(
_: Query<&mut D, Or<((With<A>, With<B>), (With<B>, With<C>))>>,
_: Query<&mut D, Or<(Without<A>, Without<B>)>>,
) {
}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn or_doesnt_remove_unrelated_filter_with() {
fn sys(_: Query<&mut B, (Or<(With<A>, With<B>)>, With<A>)>, _: Query<&mut B, Without<A>>) {}
@ -1331,4 +1732,105 @@ mod tests {
let mut world = World::new();
run_system(&mut world, || panic!("this system panics"));
}
#[test]
fn assert_systems() {
use std::str::FromStr;
use crate::{prelude::*, system::assert_is_system};
/// Mocks a system that returns a value of type `T`.
fn returning<T>() -> T {
unimplemented!()
}
/// Mocks an exclusive system that takes an input and returns an output.
fn exclusive_in_out<A, B>(_: In<A>, _: &mut World) -> B {
unimplemented!()
}
fn not(In(val): In<bool>) -> bool {
!val
}
assert_is_system(returning::<Result<u32, std::io::Error>>.pipe(unwrap));
assert_is_system(returning::<Option<()>>.pipe(ignore));
assert_is_system(returning::<&str>.pipe(new(u64::from_str)).pipe(unwrap));
assert_is_system(exclusive_in_out::<(), Result<(), std::io::Error>>.pipe(error));
assert_is_system(returning::<bool>.pipe(exclusive_in_out::<bool, ()>));
returning::<()>.run_if(returning::<bool>.pipe(not));
}
#[test]
fn pipe_change_detection() {
#[derive(Resource, Default)]
struct Flag;
#[derive(Default)]
struct Info {
// If true, the respective system will mutate `Flag`.
do_first: bool,
do_second: bool,
// Will be set to true if the respective system saw that `Flag` changed.
first_flag: bool,
second_flag: bool,
}
fn first(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
if flag.is_changed() {
info.first_flag = true;
}
if info.do_first {
*flag = Flag;
}
info
}
fn second(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
if flag.is_changed() {
info.second_flag = true;
}
if info.do_second {
*flag = Flag;
}
info
}
let mut world = World::new();
world.init_resource::<Flag>();
let mut sys = first.pipe(second);
sys.initialize(&mut world);
sys.run(default(), &mut world);
// The second system should observe a change made in the first system.
let info = sys.run(
Info {
do_first: true,
..default()
},
&mut world,
);
assert!(!info.first_flag);
assert!(info.second_flag);
// When a change is made in the second system, the first system
// should observe it the next time they are run.
let info1 = sys.run(
Info {
do_second: true,
..default()
},
&mut world,
);
let info2 = sys.run(default(), &mut world);
assert!(!info1.first_flag);
assert!(!info1.second_flag);
assert!(info2.first_flag);
assert!(!info2.second_flag);
}
}

View file

@ -49,11 +49,17 @@ pub trait System: Send + Sync + 'static {
/// 1. This system is the only system running on the given world across all threads.
/// 2. This system only runs in parallel with other systems that do not conflict with the
/// [`System::archetype_component_access()`].
///
/// Additionally, the method [`Self::update_archetype_component_access`] must be called at some
/// point before this one, with the same exact [`World`]. If `update_archetype_component_access`
/// panics (or otherwise does not return for any reason), this method must not be called.
unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out;
/// Runs the system with the given input in the world.
fn run(&mut self, input: Self::In, world: &mut World) -> Self::Out {
self.update_archetype_component_access(world);
// SAFETY: world and resources are exclusively borrowed
// SAFETY:
// - World and resources are exclusively borrowed, which ensures no data access conflicts.
// - `update_archetype_component_access` has been called.
unsafe { self.run_unsafe(input, world) }
}
fn apply_buffers(&mut self, world: &mut World);

View file

@ -1,422 +0,0 @@
use crate::system::{IntoSystem, System};
use std::borrow::Cow;
use super::{CombinatorSystem, Combine};
/// A [`System`] created by piping the output of the first system into the input of the second.
///
/// This can be repeated indefinitely, but system pipes cannot branch: the output is consumed by the receiving system.
///
/// Given two systems `A` and `B`, A may be piped into `B` as `A.pipe(B)` if the output type of `A` is
/// equal to the input type of `B`.
///
/// Note that for [`FunctionSystem`](crate::system::FunctionSystem)s the output is the return value
/// of the function and the input is the first [`SystemParam`](crate::system::SystemParam) if it is
/// tagged with [`In`](crate::system::In) or `()` if the function has no designated input parameter.
///
/// # Examples
///
/// ```
/// use std::num::ParseIntError;
///
/// use bevy_ecs::prelude::*;
///
/// fn main() {
/// let mut world = World::default();
/// world.insert_resource(Message("42".to_string()));
///
/// // pipe the `parse_message_system`'s output into the `filter_system`s input
/// let mut piped_system = parse_message_system.pipe(filter_system);
/// piped_system.initialize(&mut world);
/// assert_eq!(piped_system.run((), &mut world), Some(42));
/// }
///
/// #[derive(Resource)]
/// struct Message(String);
///
/// fn parse_message_system(message: Res<Message>) -> Result<usize, ParseIntError> {
/// message.0.parse::<usize>()
/// }
///
/// fn filter_system(In(result): In<Result<usize, ParseIntError>>) -> Option<usize> {
/// result.ok().filter(|&n| n < 100)
/// }
/// ```
pub type PipeSystem<SystemA, SystemB> = CombinatorSystem<Pipe, SystemA, SystemB>;
#[doc(hidden)]
pub struct Pipe;
impl<A, B> Combine<A, B> for Pipe
where
A: System,
B: System<In = A::Out>,
{
type In = A::In;
type Out = B::Out;
fn combine(
input: Self::In,
a: impl FnOnce(<A as System>::In) -> <A as System>::Out,
b: impl FnOnce(<B as System>::In) -> <B as System>::Out,
) -> Self::Out {
let value = a(input);
b(value)
}
}
/// An extension trait providing the [`IntoPipeSystem::pipe`] method to pass input from one system into the next.
///
/// The first system must have return type `T`
/// and the second system must have [`In<T>`](crate::system::In) as its first system parameter.
///
/// This trait is blanket implemented for all system pairs that fulfill the type requirements.
///
/// See [`PipeSystem`].
pub trait IntoPipeSystem<ParamA, Payload, SystemB, ParamB, Out>:
IntoSystem<(), Payload, ParamA> + Sized
where
SystemB: IntoSystem<Payload, Out, ParamB>,
{
/// Pass the output of this system `A` into a second system `B`, creating a new compound system.
fn pipe(self, system: SystemB) -> PipeSystem<Self::System, SystemB::System>;
}
impl<SystemA, ParamA, Payload, SystemB, ParamB, Out>
IntoPipeSystem<ParamA, Payload, SystemB, ParamB, Out> for SystemA
where
SystemA: IntoSystem<(), Payload, ParamA>,
SystemB: IntoSystem<Payload, Out, ParamB>,
{
fn pipe(self, system: SystemB) -> PipeSystem<SystemA::System, SystemB::System> {
let system_a = IntoSystem::into_system(self);
let system_b = IntoSystem::into_system(system);
let name = format!("Pipe({}, {})", system_a.name(), system_b.name());
PipeSystem::new(system_a, system_b, Cow::Owned(name))
}
}
/// A collection of common adapters for [piping](super::PipeSystem) the result of a system.
pub mod adapter {
use crate::system::In;
use bevy_utils::tracing;
use std::fmt::Debug;
/// Converts a regular function into a system adapter.
///
/// # Examples
/// ```
/// use bevy_ecs::prelude::*;
///
/// fn return1() -> u64 { 1 }
///
/// return1
/// .pipe(system_adapter::new(u32::try_from))
/// .pipe(system_adapter::unwrap)
/// .pipe(print);
///
/// fn print(In(x): In<impl std::fmt::Debug>) {
/// println!("{x:?}");
/// }
/// ```
pub fn new<T, U>(mut f: impl FnMut(T) -> U) -> impl FnMut(In<T>) -> U {
move |In(x)| f(x)
}
/// System adapter that unwraps the `Ok` variant of a [`Result`].
/// This is useful for fallible systems that should panic in the case of an error.
///
/// There is no equivalent adapter for [`Option`]. Instead, it's best to provide
/// an error message and convert to a `Result` using `ok_or{_else}`.
///
/// # Examples
///
/// Panicking on error
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Panic if the load system returns an error.
/// load_save_system.pipe(system_adapter::unwrap)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system which may fail irreparably.
/// fn load_save_system() -> Result<(), std::io::Error> {
/// let save_file = open_file("my_save.json")?;
/// dbg!(save_file);
/// Ok(())
/// }
/// # fn open_file(name: &str) -> Result<&'static str, std::io::Error>
/// # { Ok("hello world") }
/// ```
pub fn unwrap<T, E: Debug>(In(res): In<Result<T, E>>) -> T {
res.unwrap()
}
/// System adapter that utilizes the [`bevy_utils::tracing::info!`] macro to print system information.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints system information.
/// data_pipe_system.pipe(system_adapter::info)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a String output.
/// fn data_pipe_system() -> String {
/// "42".to_string()
/// }
/// ```
pub fn info<T: Debug>(In(data): In<T>) {
tracing::info!("{:?}", data);
}
/// System adapter that utilizes the [`bevy_utils::tracing::debug!`] macro to print the output of a system.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints debug data from system.
/// parse_message_system.pipe(system_adapter::dbg)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a Result<usize, String> output.
/// fn parse_message_system() -> Result<usize, std::num::ParseIntError> {
/// Ok("42".parse()?)
/// }
/// ```
pub fn dbg<T: Debug>(In(data): In<T>) {
tracing::debug!("{:?}", data);
}
/// System adapter that utilizes the [`bevy_utils::tracing::warn!`] macro to print the output of a system.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Building a new schedule/app...
/// # let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints system warning if system returns an error.
/// warning_pipe_system.pipe(system_adapter::warn)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a Result<(), String> output.
/// fn warning_pipe_system() -> Result<(), String> {
/// Err("Got to rusty?".to_string())
/// }
/// ```
pub fn warn<E: Debug>(In(res): In<Result<(), E>>) {
if let Err(warn) = res {
tracing::warn!("{:?}", warn);
}
}
/// System adapter that utilizes the [`bevy_utils::tracing::error!`] macro to print the output of a system.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
/// // Building a new schedule/app...
/// let mut sched = Schedule::default();
/// sched.add_systems(
/// // Prints system error if system fails.
/// parse_error_message_system.pipe(system_adapter::error)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system that returns a Result<())> output.
/// fn parse_error_message_system() -> Result<(), String> {
/// Err("Some error".to_owned())
/// }
/// ```
pub fn error<E: Debug>(In(res): In<Result<(), E>>) {
if let Err(error) = res {
tracing::error!("{:?}", error);
}
}
/// System adapter that ignores the output of the previous system in a pipe.
/// This is useful for fallible systems that should simply return early in case of an `Err`/`None`.
///
/// # Examples
///
/// Returning early
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Marker component for an enemy entity.
/// #[derive(Component)]
/// struct Monster;
///
/// // Building a new schedule/app...
/// # let mut sched = Schedule::default(); sched
/// .add_systems(
/// // If the system fails, just move on and try again next frame.
/// fallible_system.pipe(system_adapter::ignore)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system which may return early. It's more convenient to use the `?` operator for this.
/// fn fallible_system(
/// q: Query<Entity, With<Monster>>
/// ) -> Option<()> {
/// let monster_id = q.iter().next()?;
/// println!("Monster entity is {monster_id:?}");
/// Some(())
/// }
/// ```
pub fn ignore<T>(In(_): In<T>) {}
}
#[cfg(test)]
mod tests {
use bevy_utils::default;
use super::adapter::*;
use crate::{self as bevy_ecs, prelude::*, system::PipeSystem};
#[test]
fn assert_systems() {
use std::str::FromStr;
use crate::{prelude::*, system::assert_is_system};
/// Mocks a system that returns a value of type `T`.
fn returning<T>() -> T {
unimplemented!()
}
/// Mocks an exclusive system that takes an input and returns an output.
fn exclusive_in_out<A, B>(_: In<A>, _: &mut World) -> B {
unimplemented!()
}
fn not(In(val): In<bool>) -> bool {
!val
}
assert_is_system(returning::<Result<u32, std::io::Error>>.pipe(unwrap));
assert_is_system(returning::<Option<()>>.pipe(ignore));
assert_is_system(returning::<&str>.pipe(new(u64::from_str)).pipe(unwrap));
assert_is_system(exclusive_in_out::<(), Result<(), std::io::Error>>.pipe(error));
assert_is_system(returning::<bool>.pipe(exclusive_in_out::<bool, ()>));
returning::<()>.run_if(returning::<bool>.pipe(not));
}
#[test]
fn pipe_change_detection() {
#[derive(Resource, Default)]
struct Flag;
#[derive(Default)]
struct Info {
// If true, the respective system will mutate `Flag`.
do_first: bool,
do_second: bool,
// Will be set to true if the respective system saw that `Flag` changed.
first_flag: bool,
second_flag: bool,
}
fn first(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
if flag.is_changed() {
info.first_flag = true;
}
if info.do_first {
*flag = Flag;
}
info
}
fn second(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
if flag.is_changed() {
info.second_flag = true;
}
if info.do_second {
*flag = Flag;
}
info
}
let mut world = World::new();
world.init_resource::<Flag>();
let mut sys = PipeSystem::new(
IntoSystem::into_system(first),
IntoSystem::into_system(second),
"".into(),
);
sys.initialize(&mut world);
sys.run(default(), &mut world);
// The second system should observe a change made in the first system.
let info = sys.run(
Info {
do_first: true,
..default()
},
&mut world,
);
assert!(!info.first_flag);
assert!(info.second_flag);
// When a change is made in the second system, the first system
// should observe it the next time they are run.
let info1 = sys.run(
Info {
do_second: true,
..default()
},
&mut world,
);
let info2 = sys.run(default(), &mut world);
assert!(!info1.first_flag);
assert!(!info1.second_flag);
assert!(info2.first_flag);
assert!(!info2.second_flag);
}
}

View file

@ -63,7 +63,7 @@ pub struct World {
pub(crate) storages: Storages,
pub(crate) bundles: Bundles,
pub(crate) removed_components: RemovedComponentEvents,
/// Access cache used by [WorldCell]. Is only accessed in the `Drop` impl of `WorldCell`.
/// Access cache used by [`WorldCell`]. Is only accessed in the `Drop` impl of `WorldCell`.
pub(crate) archetype_component_access: ArchetypeComponentAccess,
pub(crate) change_tick: AtomicU32,
pub(crate) last_change_tick: Tick,
@ -1734,30 +1734,10 @@ impl World {
/// For other use cases, see the example on [`World::schedule_scope`].
pub fn try_schedule_scope<R>(
&mut self,
label: impl ScheduleLabel,
f: impl FnOnce(&mut World, &mut Schedule) -> R,
) -> Result<R, TryRunScheduleError> {
self.try_schedule_scope_ref(&label, f)
}
/// Temporarily removes the schedule associated with `label` from the world,
/// runs user code, and finally re-adds the schedule.
/// This returns a [`TryRunScheduleError`] if there is no schedule
/// associated with `label`.
///
/// Unlike the `try_run_schedule` method, this method takes the label by reference, which can save a clone.
///
/// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label,
/// and system state is cached.
///
/// For simple cases where you just need to call the schedule once,
/// consider using [`World::try_run_schedule_ref`] instead.
/// For other use cases, see the example on [`World::schedule_scope`].
pub fn try_schedule_scope_ref<R>(
&mut self,
label: &dyn ScheduleLabel,
label: impl AsRef<dyn ScheduleLabel>,
f: impl FnOnce(&mut World, &mut Schedule) -> R,
) -> Result<R, TryRunScheduleError> {
let label = label.as_ref();
let Some((extracted_label, mut schedule))
= self.get_resource_mut::<Schedules>().and_then(|mut s| s.remove_entry(label))
else {
@ -1818,33 +1798,10 @@ impl World {
/// If the requested schedule does not exist.
pub fn schedule_scope<R>(
&mut self,
label: impl ScheduleLabel,
label: impl AsRef<dyn ScheduleLabel>,
f: impl FnOnce(&mut World, &mut Schedule) -> R,
) -> R {
self.schedule_scope_ref(&label, f)
}
/// Temporarily removes the schedule associated with `label` from the world,
/// runs user code, and finally re-adds the schedule.
///
/// Unlike the `run_schedule` method, this method takes the label by reference, which can save a clone.
///
/// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label,
/// and system state is cached.
///
/// For simple cases where you just need to call the schedule,
/// consider using [`World::run_schedule_ref`] instead.
/// For other use cases, see the example on [`World::schedule_scope`].
///
/// # Panics
///
/// If the requested schedule does not exist.
pub fn schedule_scope_ref<R>(
&mut self,
label: &dyn ScheduleLabel,
f: impl FnOnce(&mut World, &mut Schedule) -> R,
) -> R {
self.try_schedule_scope_ref(label, f)
self.try_schedule_scope(label, f)
.unwrap_or_else(|e| panic!("{e}"))
}
@ -1857,25 +1814,9 @@ impl World {
/// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead.
pub fn try_run_schedule(
&mut self,
label: impl ScheduleLabel,
label: impl AsRef<dyn ScheduleLabel>,
) -> Result<(), TryRunScheduleError> {
self.try_run_schedule_ref(&label)
}
/// Attempts to run the [`Schedule`] associated with the `label` a single time,
/// and returns a [`TryRunScheduleError`] if the schedule does not exist.
///
/// Unlike the `try_run_schedule` method, this method takes the label by reference, which can save a clone.
///
/// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label,
/// and system state is cached.
///
/// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead.
pub fn try_run_schedule_ref(
&mut self,
label: &dyn ScheduleLabel,
) -> Result<(), TryRunScheduleError> {
self.try_schedule_scope_ref(label, |world, sched| sched.run(world))
self.try_schedule_scope(label, |world, sched| sched.run(world))
}
/// Runs the [`Schedule`] associated with the `label` a single time.
@ -1888,8 +1829,8 @@ impl World {
/// # Panics
///
/// If the requested schedule does not exist.
pub fn run_schedule(&mut self, label: impl ScheduleLabel) {
self.run_schedule_ref(&label);
pub fn run_schedule(&mut self, label: impl AsRef<dyn ScheduleLabel>) {
self.schedule_scope(label, |world, sched| sched.run(world));
}
/// Runs the [`Schedule`] associated with the `label` a single time.
@ -1904,8 +1845,9 @@ impl World {
/// # Panics
///
/// If the requested schedule does not exist.
#[deprecated = "Use `World::run_schedule` instead."]
pub fn run_schedule_ref(&mut self, label: &dyn ScheduleLabel) {
self.schedule_scope_ref(label, |world, sched| sched.run(world));
self.schedule_scope(label, |world, sched| sched.run(world));
}
}

View file

@ -21,6 +21,9 @@ error[E0499]: cannot borrow `e_mut` as mutable more than once at a time
error[E0505]: cannot move out of `e_mut` because it is borrowed
--> tests/ui/entity_ref_mut_lifetime_safety.rs:33:9
|
13 | let mut e_mut = world.entity_mut(e);
| --------- binding `e_mut` declared here
...
32 | let gotten: &A = e_mut.get::<A>().unwrap();
| ---------------- borrow of `e_mut` occurs here
33 | e_mut.despawn();

View file

@ -30,7 +30,7 @@ note: required by a bound in `assert_readonly`
--> tests/ui/system_param_derive_readonly.rs:23:8
|
21 | fn assert_readonly<P>()
| --------------- required by a bound in this
| --------------- required by a bound in this function
22 | where
23 | P: ReadOnlySystemParam,
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_readonly`

View file

@ -13,7 +13,10 @@ keywords = ["bevy"]
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.11.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.11.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.11.0-dev" }
# other
gilrs = "0.10.1"
thiserror = "1.0"

View file

@ -2,17 +2,23 @@
mod converter;
mod gilrs_system;
mod rumble;
use bevy_app::{App, Plugin, PreStartup, PreUpdate};
use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
use bevy_ecs::prelude::*;
use bevy_input::InputSystem;
use bevy_utils::tracing::error;
use gilrs::GilrsBuilder;
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
use rumble::{play_gilrs_rumble, RunningRumbleEffects};
#[derive(Default)]
pub struct GilrsPlugin;
/// Updates the running gamepad rumble effects.
#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
pub struct RumbleSystem;
impl Plugin for GilrsPlugin {
fn build(&self, app: &mut App) {
match GilrsBuilder::new()
@ -22,8 +28,10 @@ impl Plugin for GilrsPlugin {
{
Ok(gilrs) => {
app.insert_non_send_resource(gilrs)
.init_non_send_resource::<RunningRumbleEffects>()
.add_systems(PreStartup, gilrs_event_startup_system)
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem));
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem))
.add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystem));
}
Err(err) => error!("Failed to start Gilrs. {}", err),
}

View file

@ -0,0 +1,177 @@
//! Handle user specified rumble request events.
use bevy_ecs::{
prelude::{EventReader, Res},
system::NonSendMut,
};
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
use bevy_log::{debug, warn};
use bevy_time::Time;
use bevy_utils::{Duration, HashMap};
use gilrs::{
ff::{self, BaseEffect, BaseEffectType, Repeat, Replay},
GamepadId, Gilrs,
};
use thiserror::Error;
use crate::converter::convert_gamepad_id;
/// A rumble effect that is currently in effect.
struct RunningRumble {
/// Duration from app startup when this effect will be finished
deadline: Duration,
/// A ref-counted handle to the specific force-feedback effect
///
/// Dropping it will cause the effect to stop
#[allow(dead_code)]
effect: ff::Effect,
}
#[derive(Error, Debug)]
enum RumbleError {
#[error("gamepad not found")]
GamepadNotFound,
#[error("gilrs error while rumbling gamepad: {0}")]
GilrsError(#[from] ff::Error),
}
/// Contains the gilrs rumble effects that are currently running for each gamepad
#[derive(Default)]
pub(crate) struct RunningRumbleEffects {
/// If multiple rumbles are running at the same time, their resulting rumble
/// will be the saturated sum of their strengths up until [`u16::MAX`]
rumbles: HashMap<GamepadId, Vec<RunningRumble>>,
}
/// gilrs uses magnitudes from 0 to [`u16::MAX`], while ours go from `0.0` to `1.0` ([`f32`])
fn to_gilrs_magnitude(ratio: f32) -> u16 {
(ratio * u16::MAX as f32) as u16
}
fn get_base_effects(
GamepadRumbleIntensity {
weak_motor,
strong_motor,
}: GamepadRumbleIntensity,
duration: Duration,
) -> Vec<ff::BaseEffect> {
let mut effects = Vec::new();
if strong_motor > 0. {
effects.push(BaseEffect {
kind: BaseEffectType::Strong {
magnitude: to_gilrs_magnitude(strong_motor),
},
scheduling: Replay {
play_for: duration.into(),
..Default::default()
},
..Default::default()
});
}
if weak_motor > 0. {
effects.push(BaseEffect {
kind: BaseEffectType::Strong {
magnitude: to_gilrs_magnitude(weak_motor),
},
..Default::default()
});
}
effects
}
fn handle_rumble_request(
running_rumbles: &mut RunningRumbleEffects,
gilrs: &mut Gilrs,
rumble: GamepadRumbleRequest,
current_time: Duration,
) -> Result<(), RumbleError> {
let gamepad = rumble.gamepad();
let (gamepad_id, _) = gilrs
.gamepads()
.find(|(pad_id, _)| convert_gamepad_id(*pad_id) == gamepad)
.ok_or(RumbleError::GamepadNotFound)?;
match rumble {
GamepadRumbleRequest::Stop { .. } => {
// `ff::Effect` uses RAII, dropping = deactivating
running_rumbles.rumbles.remove(&gamepad_id);
}
GamepadRumbleRequest::Add {
duration,
intensity,
..
} => {
let mut effect_builder = ff::EffectBuilder::new();
for effect in get_base_effects(intensity, duration) {
effect_builder.add_effect(effect);
effect_builder.repeat(Repeat::For(duration.into()));
}
let effect = effect_builder.gamepads(&[gamepad_id]).finish(gilrs)?;
effect.play()?;
let gamepad_rumbles = running_rumbles.rumbles.entry(gamepad_id).or_default();
let deadline = current_time + duration;
gamepad_rumbles.push(RunningRumble { deadline, effect });
}
}
Ok(())
}
pub(crate) fn play_gilrs_rumble(
time: Res<Time>,
mut gilrs: NonSendMut<Gilrs>,
mut requests: EventReader<GamepadRumbleRequest>,
mut running_rumbles: NonSendMut<RunningRumbleEffects>,
) {
let current_time = time.raw_elapsed();
// Remove outdated rumble effects.
for rumbles in running_rumbles.rumbles.values_mut() {
// `ff::Effect` uses RAII, dropping = deactivating
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
}
running_rumbles
.rumbles
.retain(|_gamepad, rumbles| !rumbles.is_empty());
// Add new effects.
for rumble in requests.iter().cloned() {
let gamepad = rumble.gamepad();
match handle_rumble_request(&mut running_rumbles, &mut gilrs, rumble, current_time) {
Ok(()) => {}
Err(RumbleError::GilrsError(err)) => {
if let ff::Error::FfNotSupported(_) = err {
debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback");
} else {
warn!(
"Tried to handle rumble request for {gamepad:?} but an error occurred: {err}"
);
}
}
Err(RumbleError::GamepadNotFound) => {
warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!");
}
};
}
}
#[cfg(test)]
mod tests {
use super::to_gilrs_magnitude;
#[test]
fn magnitude_conversion() {
assert_eq!(to_gilrs_magnitude(1.0), u16::MAX);
assert_eq!(to_gilrs_magnitude(0.0), 0);
// bevy magnitudes of 2.0 don't really make sense, but just make sure
// they convert to something sensible in gilrs anyway.
assert_eq!(to_gilrs_magnitude(2.0), u16::MAX);
// negative bevy magnitudes don't really make sense, but just make sure
// they convert to something sensible in gilrs anyway.
assert_eq!(to_gilrs_magnitude(-1.0), 0);
assert_eq!(to_gilrs_magnitude(-0.1), 0);
}
}

View file

@ -21,3 +21,4 @@ bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.11.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.11.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.11.0-dev" }

View file

@ -7,7 +7,8 @@ use bevy_ecs::{
world::World,
};
use bevy_math::{Mat2, Quat, Vec2, Vec3};
use bevy_render::prelude::Color;
use bevy_render::color::Color;
use bevy_transform::TransformPoint;
type PositionItem = [f32; 3];
type ColorItem = [f32; 4];
@ -280,27 +281,31 @@ impl<'s> Gizmos<'s> {
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use bevy_transform::prelude::*;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.cuboid(Vec3::ZERO, Quat::IDENTITY, Vec3::ONE, Color::GREEN);
/// gizmos.cuboid(Transform::IDENTITY, Color::GREEN);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn cuboid(&mut self, position: Vec3, rotation: Quat, size: Vec3, color: Color) {
let rect = rect_inner(size.truncate());
pub fn cuboid(&mut self, transform: impl TransformPoint, color: Color) {
let rect = rect_inner(Vec2::ONE);
// Front
let [tlf, trf, brf, blf] = rect.map(|vec2| position + rotation * vec2.extend(size.z / 2.));
let [tlf, trf, brf, blf] = rect.map(|vec2| transform.transform_point(vec2.extend(0.5)));
// Back
let [tlb, trb, brb, blb] = rect.map(|vec2| position + rotation * vec2.extend(-size.z / 2.));
let [tlb, trb, brb, blb] = rect.map(|vec2| transform.transform_point(vec2.extend(-0.5)));
let positions = [
tlf, trf, trf, brf, brf, blf, blf, tlf, // Front
tlb, trb, trb, brb, brb, blb, blb, tlb, // Back
tlf, tlb, trf, trb, brf, brb, blf, blb, // Front to back
let strip_positions = [
tlf, trf, brf, blf, tlf, // Front
tlb, trb, brb, blb, tlb, // Back
];
self.extend_list_positions(positions);
self.add_list_color(color, 24);
self.linestrip(strip_positions, color);
let list_positions = [
trf, trb, brf, brb, blf, blb, // Front to back
];
self.extend_list_positions(list_positions);
self.add_list_color(color, 6);
}
/// Draw a line from `start` to `end`.
@ -459,6 +464,51 @@ impl<'s> Gizmos<'s> {
}
}
/// Draw an arc, which is a part of the circumference of a circle.
///
/// # Arguments
/// - `position` sets the center of this circle.
/// - `radius` controls the distance from `position` to this arc, and thus its curvature.
/// - `direction_angle` sets the angle in radians between `position` and the midpoint of the arc.
/// -`arc_angle` sets the length of this arc, in radians.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use std::f32::consts::PI;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.arc_2d(Vec2::ZERO, 0., PI / 4., 1., Color::GREEN);
///
/// // Arcs have 32 line-segments by default.
/// // You may want to increase this for larger arcs.
/// gizmos
/// .arc_2d(Vec2::ZERO, 0., PI / 4., 5., Color::RED)
/// .segments(64);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn arc_2d(
&mut self,
position: Vec2,
direction_angle: f32,
arc_angle: f32,
radius: f32,
color: Color,
) -> Arc2dBuilder<'_, 's> {
Arc2dBuilder {
gizmos: self,
position,
direction_angle,
arc_angle,
radius,
color,
segments: None,
}
}
/// Draw a wireframe rectangle.
///
/// # Example
@ -589,6 +639,54 @@ impl Drop for Circle2dBuilder<'_, '_> {
}
}
/// A builder returned by [`Gizmos::arc_2d`].
pub struct Arc2dBuilder<'a, 's> {
gizmos: &'a mut Gizmos<'s>,
position: Vec2,
direction_angle: f32,
arc_angle: f32,
radius: f32,
color: Color,
segments: Option<usize>,
}
impl Arc2dBuilder<'_, '_> {
/// Set the number of line-segements for this arc.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = Some(segments);
self
}
}
impl Drop for Arc2dBuilder<'_, '_> {
fn drop(&mut self) {
let segments = match self.segments {
Some(segments) => segments,
// Do a linear interpolation between 1 and `DEFAULT_CIRCLE_SEGMENTS`
// using the arc angle as scalar.
None => ((self.arc_angle.abs() / TAU) * DEFAULT_CIRCLE_SEGMENTS as f32).ceil() as usize,
};
let positions = arc_inner(self.direction_angle, self.arc_angle, self.radius, segments)
.map(|vec2| (vec2 + self.position));
self.gizmos.linestrip_2d(positions, self.color);
}
}
fn arc_inner(
direction_angle: f32,
arc_angle: f32,
radius: f32,
segments: usize,
) -> impl Iterator<Item = Vec2> {
(0..segments + 1).map(move |i| {
let start = direction_angle - arc_angle / 2.;
let angle = start + (i as f32 * (arc_angle / segments as f32));
Vec2::from(angle.sin_cos()) * radius
})
}
fn circle_inner(radius: f32, segments: usize) -> impl Iterator<Item = Vec2> {
(0..segments + 1).map(move |i| {
let angle = i as f32 * TAU / segments as f32;

View file

@ -18,22 +18,31 @@
use std::mem;
use bevy_app::{Last, Plugin};
use bevy_app::{Last, Plugin, Update};
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
use bevy_ecs::{
prelude::{Component, DetectChanges},
change_detection::DetectChanges,
component::Component,
entity::Entity,
query::Without,
reflect::ReflectComponent,
schedule::IntoSystemConfigs,
system::{Commands, Res, ResMut, Resource},
system::{Commands, Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_math::Mat4;
use bevy_reflect::TypeUuid;
use bevy_reflect::{
std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect, TypeUuid,
};
use bevy_render::{
color::Color,
mesh::Mesh,
primitives::Aabb,
render_phase::AddRenderCommand,
render_resource::{PrimitiveTopology, Shader, SpecializedMeshPipelines},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::{GlobalTransform, Transform};
#[cfg(feature = "bevy_pbr")]
use bevy_pbr::MeshUniform;
@ -47,12 +56,12 @@ mod pipeline_2d;
#[cfg(feature = "bevy_pbr")]
mod pipeline_3d;
use crate::gizmos::GizmoStorage;
use gizmos::{GizmoStorage, Gizmos};
/// The `bevy_gizmos` prelude.
pub mod prelude {
#[doc(hidden)]
pub use crate::{gizmos::Gizmos, GizmoConfig};
pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig};
}
const LINE_SHADER_HANDLE: HandleUntyped =
@ -68,7 +77,14 @@ impl Plugin for GizmoPlugin {
app.init_resource::<MeshHandles>()
.init_resource::<GizmoConfig>()
.init_resource::<GizmoStorage>()
.add_systems(Last, update_gizmo_meshes);
.add_systems(Last, update_gizmo_meshes)
.add_systems(
Update,
(
draw_aabbs,
draw_all_aabbs.run_if(|config: Res<GizmoConfig>| config.aabb.draw_all),
),
);
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; };
@ -101,7 +117,7 @@ impl Plugin for GizmoPlugin {
}
/// A [`Resource`] that stores configuration for gizmos.
#[derive(Resource, Clone, Copy)]
#[derive(Resource, Clone)]
pub struct GizmoConfig {
/// Set to `false` to stop drawing gizmos.
///
@ -113,6 +129,8 @@ pub struct GizmoConfig {
///
/// Defaults to `false`.
pub on_top: bool,
/// Configuration for the [`AabbGizmo`].
pub aabb: AabbGizmoConfig,
}
impl Default for GizmoConfig {
@ -120,23 +138,90 @@ impl Default for GizmoConfig {
Self {
enabled: true,
on_top: false,
aabb: Default::default(),
}
}
}
/// Configuration for drawing the [`Aabb`] component on entities.
#[derive(Clone, Default)]
pub struct AabbGizmoConfig {
/// Draws all bounding boxes in the scene when set to `true`.
///
/// To draw a specific entity's bounding box, you can add the [`AabbGizmo`] component.
///
/// Defaults to `false`.
pub draw_all: bool,
/// The default color for bounding box gizmos.
///
/// A random color is chosen per box if `None`.
///
/// Defaults to `None`.
pub default_color: Option<Color>,
}
/// Add this [`Component`] to an entity to draw its [`Aabb`] component.
#[derive(Component, Reflect, FromReflect, Default, Debug)]
#[reflect(Component, FromReflect, Default)]
pub struct AabbGizmo {
/// The color of the box.
///
/// The default color from the [`GizmoConfig`] resource is used if `None`,
pub color: Option<Color>,
}
fn draw_aabbs(
query: Query<(Entity, &Aabb, &GlobalTransform, &AabbGizmo)>,
config: Res<GizmoConfig>,
mut gizmos: Gizmos,
) {
for (entity, &aabb, &transform, gizmo) in &query {
let color = gizmo
.color
.or(config.aabb.default_color)
.unwrap_or_else(|| color_from_entity(entity));
gizmos.cuboid(aabb_transform(aabb, transform), color);
}
}
fn draw_all_aabbs(
query: Query<(Entity, &Aabb, &GlobalTransform), Without<AabbGizmo>>,
config: Res<GizmoConfig>,
mut gizmos: Gizmos,
) {
for (entity, &aabb, &transform) in &query {
let color = config
.aabb
.default_color
.unwrap_or_else(|| color_from_entity(entity));
gizmos.cuboid(aabb_transform(aabb, transform), color);
}
}
fn color_from_entity(entity: Entity) -> Color {
let hue = entity.to_bits() as f32 * 100_000. % 360.;
Color::hsl(hue, 1., 0.5)
}
fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform {
transform
* GlobalTransform::from(
Transform::from_translation(aabb.center.into())
.with_scale((aabb.half_extents * 2.).into()),
)
}
#[derive(Resource)]
struct MeshHandles {
list: Handle<Mesh>,
strip: Handle<Mesh>,
list: Option<Handle<Mesh>>,
strip: Option<Handle<Mesh>>,
}
impl FromWorld for MeshHandles {
fn from_world(world: &mut World) -> Self {
let mut meshes = world.resource_mut::<Assets<Mesh>>();
fn from_world(_world: &mut World) -> Self {
MeshHandles {
list: meshes.add(Mesh::new(PrimitiveTopology::LineList)),
strip: meshes.add(Mesh::new(PrimitiveTopology::LineStrip)),
list: None,
strip: None,
}
}
}
@ -146,24 +231,52 @@ struct GizmoMesh;
fn update_gizmo_meshes(
mut meshes: ResMut<Assets<Mesh>>,
handles: Res<MeshHandles>,
mut handles: ResMut<MeshHandles>,
mut storage: ResMut<GizmoStorage>,
) {
let list_mesh = meshes.get_mut(&handles.list).unwrap();
if storage.list_positions.is_empty() {
handles.list = None;
} else if let Some(handle) = handles.list.as_ref() {
let list_mesh = meshes.get_mut(handle).unwrap();
let positions = mem::take(&mut storage.list_positions);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let positions = mem::take(&mut storage.list_positions);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let colors = mem::take(&mut storage.list_colors);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
let colors = mem::take(&mut storage.list_colors);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
} else {
let mut list_mesh = Mesh::new(PrimitiveTopology::LineList);
let strip_mesh = meshes.get_mut(&handles.strip).unwrap();
let positions = mem::take(&mut storage.list_positions);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let positions = mem::take(&mut storage.strip_positions);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let colors = mem::take(&mut storage.list_colors);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
let colors = mem::take(&mut storage.strip_colors);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
handles.list = Some(meshes.add(list_mesh));
}
if storage.strip_positions.is_empty() {
handles.strip = None;
} else if let Some(handle) = handles.strip.as_ref() {
let strip_mesh = meshes.get_mut(handle).unwrap();
let positions = mem::take(&mut storage.strip_positions);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let colors = mem::take(&mut storage.strip_colors);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
} else {
let mut strip_mesh = Mesh::new(PrimitiveTopology::LineStrip);
let positions = mem::take(&mut storage.strip_positions);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let colors = mem::take(&mut storage.strip_colors);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
handles.strip = Some(meshes.add(strip_mesh));
}
}
fn extract_gizmo_data(
@ -172,7 +285,7 @@ fn extract_gizmo_data(
config: Extract<Res<GizmoConfig>>,
) {
if config.is_changed() {
commands.insert_resource(**config);
commands.insert_resource(config.clone());
}
if !config.enabled {
@ -181,28 +294,33 @@ fn extract_gizmo_data(
let transform = Mat4::IDENTITY;
let inverse_transpose_model = transform.inverse().transpose();
commands.spawn_batch([&handles.list, &handles.strip].map(|handle| {
(
GizmoMesh,
#[cfg(feature = "bevy_pbr")]
(
handle.clone(),
MeshUniform {
flags: 0,
transform,
previous_transform: transform,
inverse_transpose_model,
},
),
#[cfg(feature = "bevy_sprite")]
(
Mesh2dHandle(handle.clone()),
Mesh2dUniform {
flags: 0,
transform,
inverse_transpose_model,
},
),
)
}));
commands.spawn_batch(
[handles.list.clone(), handles.strip.clone()]
.into_iter()
.flatten()
.map(move |handle| {
(
GizmoMesh,
#[cfg(feature = "bevy_pbr")]
(
handle.clone_weak(),
MeshUniform {
flags: 0,
transform,
previous_transform: transform,
inverse_transpose_model,
},
),
#[cfg(feature = "bevy_sprite")]
(
Mesh2dHandle(handle),
Mesh2dUniform {
flags: 0,
transform,
inverse_transpose_model,
},
),
)
}),
);
}

View file

@ -5,6 +5,7 @@ use bevy_animation::AnimationClip;
use bevy_utils::HashMap;
mod loader;
mod vertex_attributes;
pub use loader::*;
use bevy_app::prelude::*;
@ -12,21 +13,47 @@ use bevy_asset::{AddAsset, Handle};
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_pbr::StandardMaterial;
use bevy_reflect::{Reflect, TypeUuid};
use bevy_render::mesh::Mesh;
use bevy_render::{
mesh::{Mesh, MeshVertexAttribute},
renderer::RenderDevice,
texture::CompressedImageFormats,
};
use bevy_scene::Scene;
/// Adds support for glTF file loading to the app.
#[derive(Default)]
pub struct GltfPlugin;
pub struct GltfPlugin {
custom_vertex_attributes: HashMap<String, MeshVertexAttribute>,
}
impl GltfPlugin {
pub fn add_custom_vertex_attribute(
mut self,
name: &str,
attribute: MeshVertexAttribute,
) -> Self {
self.custom_vertex_attributes
.insert(name.to_string(), attribute);
self
}
}
impl Plugin for GltfPlugin {
fn build(&self, app: &mut App) {
app.init_asset_loader::<GltfLoader>()
.register_type::<GltfExtras>()
.add_asset::<Gltf>()
.add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>();
let supported_compressed_formats = match app.world.get_resource::<RenderDevice>() {
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::<GltfNode>()
.add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>();
}
}

View file

@ -4,7 +4,7 @@ use bevy_asset::{
};
use bevy_core::Name;
use bevy_core_pipeline::prelude::Camera3dBundle;
use bevy_ecs::{entity::Entity, prelude::FromWorld, world::World};
use bevy_ecs::{entity::Entity, world::World};
use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder};
use bevy_log::warn;
use bevy_math::{Mat4, Vec3};
@ -17,12 +17,11 @@ use bevy_render::{
color::Color,
mesh::{
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
Indices, Mesh, VertexAttributeValues,
Indices, Mesh, MeshVertexAttribute, VertexAttributeValues,
},
prelude::SpatialBundle,
primitives::Aabb,
render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor},
renderer::RenderDevice,
texture::{CompressedImageFormats, Image, ImageSampler, ImageType, TextureError},
};
use bevy_scene::Scene;
@ -32,13 +31,14 @@ use bevy_transform::components::Transform;
use bevy_utils::{HashMap, HashSet};
use gltf::{
mesh::Mode,
mesh::{util::ReadIndices, Mode},
texture::{MagFilter, MinFilter, WrappingMode},
Material, Node, Primitive,
};
use std::{collections::VecDeque, path::Path};
use thiserror::Error;
use crate::vertex_attributes::*;
use crate::{Gltf, GltfExtras, GltfNode};
/// An error that occurs when loading a glTF file.
@ -68,7 +68,8 @@ pub enum GltfError {
/// Loads glTF files with all of their data as their corresponding bevy representations.
pub struct GltfLoader {
supported_compressed_formats: CompressedImageFormats,
pub(crate) supported_compressed_formats: CompressedImageFormats,
pub(crate) custom_vertex_attributes: HashMap<String, MeshVertexAttribute>,
}
impl AssetLoader for GltfLoader {
@ -77,9 +78,7 @@ impl AssetLoader for GltfLoader {
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<()>> {
Box::pin(async move {
Ok(load_gltf(bytes, load_context, self.supported_compressed_formats).await?)
})
Box::pin(async move { Ok(load_gltf(bytes, load_context, self).await?) })
}
fn extensions(&self) -> &[&str] {
@ -87,24 +86,11 @@ impl AssetLoader for GltfLoader {
}
}
impl FromWorld for GltfLoader {
fn from_world(world: &mut World) -> Self {
let supported_compressed_formats = match world.get_resource::<RenderDevice>() {
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::all(),
};
Self {
supported_compressed_formats,
}
}
}
/// Loads an entire glTF file.
async fn load_gltf<'a, 'b>(
bytes: &'a [u8],
load_context: &'a mut LoadContext<'b>,
supported_compressed_formats: CompressedImageFormats,
loader: &GltfLoader,
) -> Result<(), GltfError> {
let gltf = gltf::Gltf::from_slice(bytes)?;
let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?;
@ -233,53 +219,31 @@ async fn load_gltf<'a, 'b>(
let mut primitives = vec![];
for primitive in mesh.primitives() {
let primitive_label = primitive_label(&mesh, &primitive);
let reader = primitive.reader(|buffer| Some(&buffer_data[buffer.index()]));
let primitive_topology = get_primitive_topology(primitive.mode())?;
let mut mesh = Mesh::new(primitive_topology);
if let Some(vertex_attribute) = reader
.read_positions()
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
{
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertex_attribute);
}
if let Some(vertex_attribute) = reader
.read_normals()
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
{
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute);
}
if let Some(vertex_attribute) = reader
.read_tex_coords(0)
.map(|v| VertexAttributeValues::Float32x2(v.into_f32().collect()))
{
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vertex_attribute);
}
if let Some(vertex_attribute) = reader
.read_colors(0)
.map(|v| VertexAttributeValues::Float32x4(v.into_rgba_f32().collect()))
{
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute);
}
if let Some(iter) = reader.read_joints(0) {
let vertex_attribute = VertexAttributeValues::Uint16x4(iter.into_u16().collect());
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, vertex_attribute);
}
if let Some(vertex_attribute) = reader
.read_weights(0)
.map(|v| VertexAttributeValues::Float32x4(v.into_f32().collect()))
{
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, vertex_attribute);
// Read vertex attributes
for (semantic, accessor) in primitive.attributes() {
match convert_attribute(
semantic,
accessor,
&buffer_data,
&loader.custom_vertex_attributes,
) {
Ok((attribute, values)) => mesh.insert_attribute(attribute, values),
Err(err) => warn!("{}", err),
}
}
// Read vertex indices
let reader = primitive.reader(|buffer| Some(buffer_data[buffer.index()].as_slice()));
if let Some(indices) = reader.read_indices() {
mesh.set_indices(Some(Indices::U32(indices.into_u32().collect())));
mesh.set_indices(Some(match indices {
ReadIndices::U8(is) => Indices::U16(is.map(|x| x as u16).collect()),
ReadIndices::U16(is) => Indices::U16(is.collect()),
ReadIndices::U32(is) => Indices::U32(is.collect()),
}));
};
if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none()
@ -403,7 +367,7 @@ async fn load_gltf<'a, 'b>(
&buffer_data,
&linear_textures,
load_context,
supported_compressed_formats,
loader.supported_compressed_formats,
)
.await?;
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
@ -422,7 +386,7 @@ async fn load_gltf<'a, 'b>(
buffer_data,
linear_textures,
load_context,
supported_compressed_formats,
loader.supported_compressed_formats,
)
.await
});

View file

@ -0,0 +1,287 @@
use bevy_render::{
mesh::{MeshVertexAttribute, VertexAttributeValues as Values},
prelude::Mesh,
render_resource::VertexFormat,
};
use bevy_utils::HashMap;
use gltf::{
accessor::{DataType, Dimensions},
mesh::util::{ReadColors, ReadJoints, ReadTexCoords},
};
use thiserror::Error;
/// Represents whether integer data requires normalization
#[derive(Copy, Clone)]
struct Normalization(bool);
impl Normalization {
fn apply_either<T, U>(
self,
value: T,
normalized_ctor: impl Fn(T) -> U,
unnormalized_ctor: impl Fn(T) -> U,
) -> U {
if self.0 {
normalized_ctor(value)
} else {
unnormalized_ctor(value)
}
}
}
/// An error that occurs when accessing buffer data
#[derive(Error, Debug)]
pub(crate) enum AccessFailed {
#[error("Malformed vertex attribute data")]
MalformedData,
#[error("Unsupported vertex attribute format")]
UnsupportedFormat,
}
/// Helper for reading buffer data
struct BufferAccessor<'a> {
accessor: gltf::Accessor<'a>,
buffer_data: &'a Vec<Vec<u8>>,
normalization: Normalization,
}
impl<'a> BufferAccessor<'a> {
/// Creates an iterator over the elements in this accessor
fn iter<T: gltf::accessor::Item>(self) -> Result<gltf::accessor::Iter<'a, T>, AccessFailed> {
gltf::accessor::Iter::new(self.accessor, |buffer: gltf::Buffer| {
self.buffer_data.get(buffer.index()).map(|v| v.as_slice())
})
.ok_or(AccessFailed::MalformedData)
}
/// Applies the element iterator to a constructor or fails if normalization is required
fn with_no_norm<T: gltf::accessor::Item, U>(
self,
ctor: impl Fn(gltf::accessor::Iter<'a, T>) -> U,
) -> Result<U, AccessFailed> {
if self.normalization.0 {
return Err(AccessFailed::UnsupportedFormat);
}
self.iter().map(ctor)
}
/// Applies the element iterator and the normalization flag to a constructor
fn with_norm<T: gltf::accessor::Item, U>(
self,
ctor: impl Fn(gltf::accessor::Iter<'a, T>, Normalization) -> U,
) -> Result<U, AccessFailed> {
let normalized = self.normalization;
self.iter().map(|v| ctor(v, normalized))
}
}
/// An enum of the iterators user by different vertex attribute formats
enum VertexAttributeIter<'a> {
// For reading native WGPU formats
F32(gltf::accessor::Iter<'a, f32>),
U32(gltf::accessor::Iter<'a, u32>),
F32x2(gltf::accessor::Iter<'a, [f32; 2]>),
U32x2(gltf::accessor::Iter<'a, [u32; 2]>),
F32x3(gltf::accessor::Iter<'a, [f32; 3]>),
U32x3(gltf::accessor::Iter<'a, [u32; 3]>),
F32x4(gltf::accessor::Iter<'a, [f32; 4]>),
U32x4(gltf::accessor::Iter<'a, [u32; 4]>),
S16x2(gltf::accessor::Iter<'a, [i16; 2]>, Normalization),
U16x2(gltf::accessor::Iter<'a, [u16; 2]>, Normalization),
S16x4(gltf::accessor::Iter<'a, [i16; 4]>, Normalization),
U16x4(gltf::accessor::Iter<'a, [u16; 4]>, Normalization),
S8x2(gltf::accessor::Iter<'a, [i8; 2]>, Normalization),
U8x2(gltf::accessor::Iter<'a, [u8; 2]>, Normalization),
S8x4(gltf::accessor::Iter<'a, [i8; 4]>, Normalization),
U8x4(gltf::accessor::Iter<'a, [u8; 4]>, Normalization),
// Additional on-disk formats used for RGB colors
U16x3(gltf::accessor::Iter<'a, [u16; 3]>, Normalization),
U8x3(gltf::accessor::Iter<'a, [u8; 3]>, Normalization),
}
impl<'a> VertexAttributeIter<'a> {
/// Creates an iterator over the elements in a vertex attribute accessor
fn from_accessor(
accessor: gltf::Accessor<'a>,
buffer_data: &'a Vec<Vec<u8>>,
) -> Result<VertexAttributeIter<'a>, AccessFailed> {
let normalization = Normalization(accessor.normalized());
let format = (accessor.data_type(), accessor.dimensions());
let acc = BufferAccessor {
accessor,
buffer_data,
normalization,
};
match format {
(DataType::F32, Dimensions::Scalar) => acc.with_no_norm(VertexAttributeIter::F32),
(DataType::U32, Dimensions::Scalar) => acc.with_no_norm(VertexAttributeIter::U32),
(DataType::F32, Dimensions::Vec2) => acc.with_no_norm(VertexAttributeIter::F32x2),
(DataType::U32, Dimensions::Vec2) => acc.with_no_norm(VertexAttributeIter::U32x2),
(DataType::F32, Dimensions::Vec3) => acc.with_no_norm(VertexAttributeIter::F32x3),
(DataType::U32, Dimensions::Vec3) => acc.with_no_norm(VertexAttributeIter::U32x3),
(DataType::F32, Dimensions::Vec4) => acc.with_no_norm(VertexAttributeIter::F32x4),
(DataType::U32, Dimensions::Vec4) => acc.with_no_norm(VertexAttributeIter::U32x4),
(DataType::I16, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::S16x2),
(DataType::U16, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::U16x2),
(DataType::I16, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::S16x4),
(DataType::U16, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::U16x4),
(DataType::I8, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::S8x2),
(DataType::U8, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::U8x2),
(DataType::I8, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::S8x4),
(DataType::U8, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::U8x4),
(DataType::U16, Dimensions::Vec3) => acc.with_norm(VertexAttributeIter::U16x3),
(DataType::U8, Dimensions::Vec3) => acc.with_norm(VertexAttributeIter::U8x3),
_ => Err(AccessFailed::UnsupportedFormat),
}
}
/// Materializes values for any supported format of vertex attribute
fn into_any_values(self) -> Result<Values, AccessFailed> {
match self {
VertexAttributeIter::F32(it) => Ok(Values::Float32(it.collect())),
VertexAttributeIter::U32(it) => Ok(Values::Uint32(it.collect())),
VertexAttributeIter::F32x2(it) => Ok(Values::Float32x2(it.collect())),
VertexAttributeIter::U32x2(it) => Ok(Values::Uint32x2(it.collect())),
VertexAttributeIter::F32x3(it) => Ok(Values::Float32x3(it.collect())),
VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())),
VertexAttributeIter::F32x4(it) => Ok(Values::Float32x4(it.collect())),
VertexAttributeIter::U32x4(it) => Ok(Values::Uint32x4(it.collect())),
VertexAttributeIter::S16x2(it, n) => {
Ok(n.apply_either(it.collect(), Values::Snorm16x2, Values::Sint16x2))
}
VertexAttributeIter::U16x2(it, n) => {
Ok(n.apply_either(it.collect(), Values::Unorm16x2, Values::Uint16x2))
}
VertexAttributeIter::S16x4(it, n) => {
Ok(n.apply_either(it.collect(), Values::Snorm16x4, Values::Sint16x4))
}
VertexAttributeIter::U16x4(it, n) => {
Ok(n.apply_either(it.collect(), Values::Unorm16x4, Values::Uint16x4))
}
VertexAttributeIter::S8x2(it, n) => {
Ok(n.apply_either(it.collect(), Values::Snorm8x2, Values::Sint8x2))
}
VertexAttributeIter::U8x2(it, n) => {
Ok(n.apply_either(it.collect(), Values::Unorm8x2, Values::Uint8x2))
}
VertexAttributeIter::S8x4(it, n) => {
Ok(n.apply_either(it.collect(), Values::Snorm8x4, Values::Sint8x4))
}
VertexAttributeIter::U8x4(it, n) => {
Ok(n.apply_either(it.collect(), Values::Unorm8x4, Values::Uint8x4))
}
_ => Err(AccessFailed::UnsupportedFormat),
}
}
/// Materializes RGBA values, converting compatible formats to Float32x4
fn into_rgba_values(self) -> Result<Values, AccessFailed> {
match self {
VertexAttributeIter::U8x3(it, Normalization(true)) => Ok(Values::Float32x4(
ReadColors::RgbU8(it).into_rgba_f32().collect(),
)),
VertexAttributeIter::U16x3(it, Normalization(true)) => Ok(Values::Float32x4(
ReadColors::RgbU16(it).into_rgba_f32().collect(),
)),
VertexAttributeIter::F32x3(it) => Ok(Values::Float32x4(
ReadColors::RgbF32(it).into_rgba_f32().collect(),
)),
VertexAttributeIter::U8x4(it, Normalization(true)) => Ok(Values::Float32x4(
ReadColors::RgbaU8(it).into_rgba_f32().collect(),
)),
VertexAttributeIter::U16x4(it, Normalization(true)) => Ok(Values::Float32x4(
ReadColors::RgbaU16(it).into_rgba_f32().collect(),
)),
s => s.into_any_values(),
}
}
/// Materializes joint index values, converting compatible formats to Uint16x4
fn into_joint_index_values(self) -> Result<Values, AccessFailed> {
match self {
VertexAttributeIter::U8x4(it, Normalization(false)) => {
Ok(Values::Uint16x4(ReadJoints::U8(it).into_u16().collect()))
}
s => s.into_any_values(),
}
}
/// Materializes texture coordinate values, converting compatible formats to Float32x2
fn into_tex_coord_values(self) -> Result<Values, AccessFailed> {
match self {
VertexAttributeIter::U8x2(it, Normalization(true)) => Ok(Values::Float32x2(
ReadTexCoords::U8(it).into_f32().collect(),
)),
VertexAttributeIter::U16x2(it, Normalization(true)) => Ok(Values::Float32x2(
ReadTexCoords::U16(it).into_f32().collect(),
)),
s => s.into_any_values(),
}
}
}
enum ConversionMode {
Any,
Rgba,
JointIndex,
TexCoord,
}
#[derive(Error, Debug)]
pub(crate) enum ConvertAttributeError {
#[error("Vertex attribute {0} has format {1:?} but expected {3:?} for target attribute {2}")]
WrongFormat(String, VertexFormat, String, VertexFormat),
#[error("{0} in accessor {1}")]
AccessFailed(AccessFailed, usize),
#[error("Unknown vertex attribute {0}")]
UnknownName(String),
}
pub(crate) fn convert_attribute(
semantic: gltf::Semantic,
accessor: gltf::Accessor,
buffer_data: &Vec<Vec<u8>>,
custom_vertex_attributes: &HashMap<String, MeshVertexAttribute>,
) -> Result<(MeshVertexAttribute, Values), ConvertAttributeError> {
if let Some((attribute, conversion)) = match &semantic {
gltf::Semantic::Positions => Some((Mesh::ATTRIBUTE_POSITION, ConversionMode::Any)),
gltf::Semantic::Normals => Some((Mesh::ATTRIBUTE_NORMAL, ConversionMode::Any)),
gltf::Semantic::Tangents => Some((Mesh::ATTRIBUTE_TANGENT, ConversionMode::Any)),
gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba)),
gltf::Semantic::TexCoords(0) => Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord)),
gltf::Semantic::Joints(0) => {
Some((Mesh::ATTRIBUTE_JOINT_INDEX, ConversionMode::JointIndex))
}
gltf::Semantic::Weights(0) => Some((Mesh::ATTRIBUTE_JOINT_WEIGHT, ConversionMode::Any)),
gltf::Semantic::Extras(name) => custom_vertex_attributes
.get(name)
.map(|attr| (attr.clone(), ConversionMode::Any)),
_ => None,
} {
let raw_iter = VertexAttributeIter::from_accessor(accessor.clone(), buffer_data);
let converted_values = raw_iter.and_then(|iter| match conversion {
ConversionMode::Any => iter.into_any_values(),
ConversionMode::Rgba => iter.into_rgba_values(),
ConversionMode::TexCoord => iter.into_tex_coord_values(),
ConversionMode::JointIndex => iter.into_joint_index_values(),
});
match converted_values {
Ok(values) => {
let loaded_format = VertexFormat::from(&values);
if attribute.format == loaded_format {
Ok((attribute, values))
} else {
Err(ConvertAttributeError::WrongFormat(
semantic.to_string(),
loaded_format,
attribute.name.to_string(),
attribute.format,
))
}
}
Err(err) => Err(ConvertAttributeError::AccessFailed(err, accessor.index())),
}
} else {
Err(ConvertAttributeError::UnknownName(semantic.to_string()))
}
}

View file

@ -16,6 +16,9 @@ fn push_events(world: &mut World, events: impl IntoIterator<Item = HierarchyEven
}
}
/// Adds `child` to `parent`'s [`Children`], without checking if it is already present there.
///
/// This might cause unexpected results when removing duplicate children.
fn push_child_unchecked(world: &mut World, parent: Entity, child: Entity) {
let mut parent = world.entity_mut(parent);
if let Some(mut children) = parent.get_mut::<Children>() {
@ -25,6 +28,7 @@ fn push_child_unchecked(world: &mut World, parent: Entity, child: Entity) {
}
}
/// Sets [`Parent`] of the `child` to `new_parent`. Inserts [`Parent`] if `child` doesn't have one.
fn update_parent(world: &mut World, child: Entity, new_parent: Entity) -> Option<Entity> {
let mut child = world.entity_mut(child);
if let Some(mut parent) = child.get_mut::<Parent>() {
@ -41,12 +45,15 @@ fn update_parent(world: &mut World, child: Entity, new_parent: Entity) -> Option
///
/// Removes the [`Children`] component from the parent if it's empty.
fn remove_from_children(world: &mut World, parent: Entity, child: Entity) {
let mut parent = world.entity_mut(parent);
if let Some(mut children) = parent.get_mut::<Children>() {
children.0.retain(|x| *x != child);
if children.is_empty() {
parent.remove::<Children>();
}
let Some(mut parent) = world.get_entity_mut(parent) else {
return;
};
let Some(mut children) = parent.get_mut::<Children>() else {
return;
};
children.0.retain(|x| *x != child);
if children.is_empty() {
parent.remove::<Children>();
}
}
@ -110,6 +117,8 @@ fn update_old_parents(world: &mut World, parent: Entity, children: &[Entity]) {
push_events(world, events);
}
/// Removes entities in `children` from `parent`'s [`Children`], removing the component if it ends up empty.
/// Also removes [`Parent`] component from `children`.
fn remove_children(parent: Entity, children: &[Entity], world: &mut World) {
let mut events: SmallVec<[HierarchyEvent; 8]> = SmallVec::new();
if let Some(parent_children) = world.get::<Children>(parent) {
@ -140,6 +149,8 @@ fn remove_children(parent: Entity, children: &[Entity], world: &mut World) {
}
}
/// Removes all children from `parent` by removing its [`Children`] component, as well as removing
/// [`Parent`] component from its children.
fn clear_children(parent: Entity, world: &mut World) {
if let Some(children) = world.entity_mut(parent).take::<Children>() {
for &child in &children.0 {
@ -148,12 +159,12 @@ fn clear_children(parent: Entity, world: &mut World) {
}
}
/// Command that adds a child to an entity
/// Command that adds a child to an entity.
#[derive(Debug)]
pub struct AddChild {
/// Parent entity to add the child to
/// Parent entity to add the child to.
pub parent: Entity,
/// Child entity to add
/// Child entity to add.
pub child: Entity,
}
@ -163,7 +174,7 @@ impl Command for AddChild {
}
}
/// Command that inserts a child at a given index of a parent's children, shifting following children back
/// Command that inserts a child at a given index of a parent's children, shifting following children back.
#[derive(Debug)]
pub struct InsertChildren {
parent: Entity,
@ -192,7 +203,7 @@ impl Command for PushChildren {
}
}
/// Command that removes children from an entity, and removes that child's parent.
/// Command that removes children from an entity, and removes these children's parent.
pub struct RemoveChildren {
parent: Entity,
children: SmallVec<[Entity; 8]>,
@ -204,7 +215,8 @@ impl Command for RemoveChildren {
}
}
/// Command that clear all children from an entity.
/// Command that clears all children from an entity and removes [`Parent`] component from those
/// children.
pub struct ClearChildren {
parent: Entity,
}
@ -215,7 +227,7 @@ impl Command for ClearChildren {
}
}
/// Command that clear all children from an entity. And replace with the given children.
/// Command that clear all children from an entity, replacing them with the given children.
pub struct ReplaceChildren {
parent: Entity,
children: SmallVec<[Entity; 8]>,
@ -240,42 +252,44 @@ impl Command for RemoveParent {
}
}
/// Struct for building children onto an entity
/// Struct for building children entities and adding them to a parent entity.
pub struct ChildBuilder<'w, 's, 'a> {
commands: &'a mut Commands<'w, 's>,
push_children: PushChildren,
}
impl<'w, 's, 'a> ChildBuilder<'w, 's, 'a> {
/// Spawns an entity with the given bundle and inserts it into the children defined by the [`ChildBuilder`]
/// Spawns an entity with the given bundle and inserts it into the parent entity's [`Children`].
/// Also adds [`Parent`] component to the created entity.
pub fn spawn(&mut self, bundle: impl Bundle) -> EntityCommands<'w, 's, '_> {
let e = self.commands.spawn(bundle);
self.push_children.children.push(e.id());
e
}
/// Spawns an [`Entity`] with no components and inserts it into the children defined by the [`ChildBuilder`] which adds the [`Parent`] component to it.
/// Spawns an [`Entity`] with no components and inserts it into the parent entity's [`Children`].
/// Also adds [`Parent`] component to the created entity.
pub fn spawn_empty(&mut self) -> EntityCommands<'w, 's, '_> {
let e = self.commands.spawn_empty();
self.push_children.children.push(e.id());
e
}
/// Returns the parent entity of this [`ChildBuilder`]
/// Returns the parent entity of this [`ChildBuilder`].
pub fn parent_entity(&self) -> Entity {
self.push_children.parent
}
/// Adds a command to this [`ChildBuilder`]
/// Adds a command to be executed, like [`Commands::add`].
pub fn add_command<C: Command + 'static>(&mut self, command: C) -> &mut Self {
self.commands.add(command);
self
}
}
/// Trait defining how to build children
/// Trait for removing, adding and replacing children and parents of an entity.
pub trait BuildChildren {
/// Creates a [`ChildBuilder`] with the given children built in the given closure
/// Takes a clousre which builds children for this entity using [`ChildBuilder`].
fn with_children(&mut self, f: impl FnOnce(&mut ChildBuilder)) -> &mut Self;
/// Pushes children to the back of the builder's children. For any entities that are
/// already a child of this one, this method does nothing.
@ -284,7 +298,7 @@ pub trait BuildChildren {
/// will have those children removed from its list. Removing all children from a parent causes its
/// [`Children`] component to be removed from the entity.
fn push_children(&mut self, children: &[Entity]) -> &mut Self;
/// Inserts children at the given index
/// Inserts children at the given index.
///
/// If the children were previously children of another parent, that parent's [`Children`] component
/// will have those children removed from its list. Removing all children from a parent causes its
@ -294,7 +308,7 @@ pub trait BuildChildren {
///
/// Removing all children from a parent causes its [`Children`] component to be removed from the entity.
fn remove_children(&mut self, children: &[Entity]) -> &mut Self;
/// Adds a single child
/// Adds a single child.
///
/// If the children were previously children of another parent, that parent's [`Children`] component
/// will have those children removed from its list. Removing all children from a parent causes its
@ -303,10 +317,19 @@ pub trait BuildChildren {
/// Removes all children from this entity. The [`Children`] component will be removed if it exists, otherwise this does nothing.
fn clear_children(&mut self) -> &mut Self;
/// Removes all current children from this entity, replacing them with the specified list of entities.
///
/// The removed children will have their [`Parent`] component removed.
fn replace_children(&mut self, children: &[Entity]) -> &mut Self;
/// Sets the parent of this entity.
///
/// If this entity already had a parent, the parent's [`Children`] component will have this
/// child removed from its list. Removing all children from a parent causes its [`Children`]
/// component to be removed from the entity.
fn set_parent(&mut self, parent: Entity) -> &mut Self;
/// Removes the parent of this entity.
/// Removes the [`Parent`] of this entity.
///
/// Also removes this entity from its parent's [`Children`] component. Removing all children from a parent causes
/// its [`Children`] component to be removed from the entity.
fn remove_parent(&mut self) -> &mut Self;
}
@ -389,7 +412,7 @@ impl<'w, 's, 'a> BuildChildren for EntityCommands<'w, 's, 'a> {
}
}
/// Struct for adding children to an entity directly through the [`World`] for use in exclusive systems
/// Struct for adding children to an entity directly through the [`World`] for use in exclusive systems.
#[derive(Debug)]
pub struct WorldChildBuilder<'w> {
world: &'w mut World,
@ -397,7 +420,8 @@ pub struct WorldChildBuilder<'w> {
}
impl<'w> WorldChildBuilder<'w> {
/// Spawns an entity with the given bundle and inserts it into the children defined by the [`WorldChildBuilder`]
/// Spawns an entity with the given bundle and inserts it into the parent entity's [`Children`].
/// Also adds [`Parent`] component to the created entity.
pub fn spawn(&mut self, bundle: impl Bundle + Send + Sync + 'static) -> EntityMut<'_> {
let entity = self.world.spawn((bundle, Parent(self.parent))).id();
push_child_unchecked(self.world, self.parent, entity);
@ -411,7 +435,8 @@ impl<'w> WorldChildBuilder<'w> {
self.world.entity_mut(entity)
}
/// Spawns an [`Entity`] with no components and inserts it into the children defined by the [`WorldChildBuilder`] which adds the [`Parent`] component to it.
/// Spawns an [`Entity`] with no components and inserts it into the parent entity's [`Children`].
/// Also adds [`Parent`] component to the created entity.
pub fn spawn_empty(&mut self) -> EntityMut<'_> {
let entity = self.world.spawn(Parent(self.parent)).id();
push_child_unchecked(self.world, self.parent, entity);
@ -425,37 +450,53 @@ impl<'w> WorldChildBuilder<'w> {
self.world.entity_mut(entity)
}
/// Returns the parent entity of this [`WorldChildBuilder`]
/// Returns the parent entity of this [`WorldChildBuilder`].
pub fn parent_entity(&self) -> Entity {
self.parent
}
}
/// Trait that defines adding children to an entity directly through the [`World`]
/// Trait that defines adding, changing and children and parents of an entity directly through the [`World`].
pub trait BuildWorldChildren {
/// Creates a [`WorldChildBuilder`] with the given children built in the given closure
/// Takes a clousre which builds children for this entity using [`WorldChildBuilder`].
fn with_children(&mut self, spawn_children: impl FnOnce(&mut WorldChildBuilder)) -> &mut Self;
/// Adds a single child
/// Adds a single child.
///
/// If the children were previously children of another parent, that parent's [`Children`] component
/// will have those children removed from its list. Removing all children from a parent causes its
/// [`Children`] component to be removed from the entity.
fn add_child(&mut self, child: Entity) -> &mut Self;
/// Pushes children to the back of the builder's children
/// Pushes children to the back of the builder's children. For any entities that are
/// already a child of this one, this method does nothing.
///
/// If the children were previously children of another parent, that parent's [`Children`] component
/// will have those children removed from its list. Removing all children from a parent causes its
/// [`Children`] component to be removed from the entity.
fn push_children(&mut self, children: &[Entity]) -> &mut Self;
/// Inserts children at the given index
/// Inserts children at the given index.
///
/// If the children were previously children of another parent, that parent's [`Children`] component
/// will have those children removed from its list. Removing all children from a parent causes its
/// [`Children`] component to be removed from the entity.
fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self;
/// Removes the given children
///
/// Removing all children from a parent causes its [`Children`] component to be removed from the entity.
fn remove_children(&mut self, children: &[Entity]) -> &mut Self;
/// Set the `parent` of this entity. This entity will be added to the end of the `parent`'s list of children.
/// Sets the parent of this entity.
///
/// If this entity already had a parent it will be removed from it.
/// If this entity already had a parent, the parent's [`Children`] component will have this
/// child removed from its list. Removing all children from a parent causes its [`Children`]
/// component to be removed from the entity.
fn set_parent(&mut self, parent: Entity) -> &mut Self;
/// Remove the parent from this entity.
/// Removes the [`Parent`] of this entity.
///
/// Also removes this entity from its parent's [`Children`] component. Removing all children from a parent causes
/// its [`Children`] component to be removed from the entity.
fn remove_parent(&mut self) -> &mut Self;
}
@ -653,6 +694,23 @@ mod tests {
);
}
// regression test for https://github.com/bevyengine/bevy/pull/8346
#[test]
fn set_parent_of_orphan() {
let world = &mut World::new();
let [a, b, c] = std::array::from_fn(|_| world.spawn_empty().id());
world.entity_mut(a).set_parent(b);
assert_parent(world, a, Some(b));
assert_children(world, b, Some(&[a]));
world.entity_mut(b).despawn();
world.entity_mut(a).set_parent(c);
assert_parent(world, a, Some(c));
assert_children(world, c, Some(&[a]));
}
#[test]
fn remove_parent() {
let world = &mut World::new();

View file

@ -46,9 +46,9 @@ fn despawn_with_children_recursive_inner(world: &mut World, entity: Entity) {
}
}
fn despawn_children(world: &mut World, entity: Entity) {
if let Some(mut children) = world.get_mut::<Children>(entity) {
for e in std::mem::take(&mut children.0) {
fn despawn_children_recursive(world: &mut World, entity: Entity) {
if let Some(children) = world.entity_mut(entity).take::<Children>() {
for e in children.0 {
despawn_with_children_recursive_inner(world, e);
}
}
@ -76,7 +76,7 @@ impl Command for DespawnChildrenRecursive {
entity = bevy_utils::tracing::field::debug(self.entity)
)
.entered();
despawn_children(world, self.entity);
despawn_children_recursive(world, self.entity);
}
}
@ -127,11 +127,9 @@ impl<'w> DespawnRecursiveExt for EntityMut<'w> {
)
.entered();
// SAFETY: The location is updated.
unsafe {
despawn_children(self.world_mut(), entity);
self.update_location();
}
self.world_scope(|world| {
despawn_children_recursive(world, entity);
});
}
}
@ -226,4 +224,26 @@ mod tests {
]
);
}
#[test]
fn despawn_descendants() {
let mut world = World::default();
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
let parent = commands.spawn_empty().id();
let child = commands.spawn_empty().id();
commands
.entity(parent)
.add_child(child)
.despawn_descendants();
queue.apply(&mut world);
// The parent's Children component should be removed.
assert!(world.entity(parent).get::<Children>().is_none());
// The child should be despawned.
assert!(world.get_entity(child).is_none());
}
}

View file

@ -45,7 +45,7 @@ impl<T> Default for ReportHierarchyIssue<T> {
}
}
/// System to print a warning for each `Entity` with a `T` component
/// System to print a warning for each [`Entity`] with a `T` component
/// which parent hasn't a `T` component.
///
/// Hierarchy propagations are top-down, and limited only to entities

View file

@ -5,6 +5,7 @@ use bevy_ecs::{
system::{Res, ResMut, Resource},
};
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect};
use bevy_utils::Duration;
use bevy_utils::{tracing::info, HashMap};
use thiserror::Error;
@ -90,7 +91,7 @@ impl Gamepad {
}
}
/// Metadata associated with a `Gamepad`.
/// Metadata associated with a [`Gamepad`].
#[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)]
#[reflect(Debug, PartialEq)]
#[cfg_attr(
@ -629,9 +630,9 @@ impl AxisSettings {
///
/// # Errors
///
/// Returns an `AxisSettingsError` if any restrictions on the zone values are not met.
/// If the zone restrictions are met, but the ``threshold`` value restrictions are not met,
/// returns `AxisSettingsError::Threshold`.
/// Returns an [`AxisSettingsError`] if any restrictions on the zone values are not met.
/// If the zone restrictions are met, but the `threshold` value restrictions are not met,
/// returns [`AxisSettingsError::Threshold`].
pub fn new(
livezone_lowerbound: f32,
deadzone_lowerbound: f32,
@ -875,7 +876,7 @@ impl AxisSettings {
}
/// Determines whether the change from `old_value` to `new_value` should
/// be registered as a change, according to the `AxisSettings`.
/// be registered as a change, according to the [`AxisSettings`].
fn should_register_change(&self, new_value: f32, old_value: Option<f32>) -> bool {
if old_value.is_none() {
return true;
@ -963,7 +964,7 @@ impl ButtonAxisSettings {
f32::abs(new_value - old_value.unwrap()) > self.threshold
}
/// Filters the `new_value` based on the `old_value`, according to the `ButtonAxisSettings`.
/// Filters the `new_value` based on the `old_value`, according to the [`ButtonAxisSettings`].
///
/// Returns the clamped `new_value`, according to the [`ButtonAxisSettings`], if the change
/// exceeds the settings threshold, and `None` otherwise.
@ -1116,7 +1117,7 @@ impl GamepadButtonChangedEvent {
}
}
/// Uses [`GamepadAxisChangedEvent`]s to update the relevant `Input` and `Axis` values.
/// Uses [`GamepadAxisChangedEvent`]s to update the relevant [`Input`] and [`Axis`] values.
pub fn gamepad_axis_event_system(
mut gamepad_axis: ResMut<Axis<GamepadAxis>>,
mut axis_events: EventReader<GamepadAxisChangedEvent>,
@ -1127,7 +1128,7 @@ pub fn gamepad_axis_event_system(
}
}
/// Uses [`GamepadButtonChangedEvent`]s to update the relevant `Input` and `Axis` values.
/// Uses [`GamepadButtonChangedEvent`]s to update the relevant [`Input`] and [`Axis`] values.
pub fn gamepad_button_event_system(
mut button_events: EventReader<GamepadButtonChangedEvent>,
mut button_input: ResMut<Input<GamepadButton>>,
@ -1240,6 +1241,127 @@ const ALL_AXIS_TYPES: [GamepadAxisType; 6] = [
GamepadAxisType::RightZ,
];
/// The intensity at which a gamepad's force-feedback motors may rumble.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GamepadRumbleIntensity {
/// The rumble intensity of the strong gamepad motor
///
/// Ranges from 0.0 to 1.0
///
/// By convention, this is usually a low-frequency motor on the left-hand
/// side of the gamepad, though it may vary across platforms and hardware.
pub strong_motor: f32,
/// The rumble intensity of the weak gamepad motor
///
/// Ranges from 0.0 to 1.0
///
/// By convention, this is usually a high-frequency motor on the right-hand
/// side of the gamepad, though it may vary across platforms and hardware.
pub weak_motor: f32,
}
impl GamepadRumbleIntensity {
/// Rumble both gamepad motors at maximum intensity
pub const MAX: Self = GamepadRumbleIntensity {
strong_motor: 1.0,
weak_motor: 1.0,
};
/// Rumble the weak motor at maximum intensity
pub const WEAK_MAX: Self = GamepadRumbleIntensity {
strong_motor: 0.0,
weak_motor: 1.0,
};
/// Rumble the strong motor at maximum intensity
pub const STRONG_MAX: Self = GamepadRumbleIntensity {
strong_motor: 1.0,
weak_motor: 0.0,
};
/// Creates a new rumble intensity with weak motor intensity set to the given value
///
/// Clamped within the 0 to 1 range
pub const fn weak_motor(intensity: f32) -> Self {
Self {
weak_motor: intensity,
strong_motor: 0.0,
}
}
/// Creates a new rumble intensity with strong motor intensity set to the given value
///
/// Clamped within the 0 to 1 range
pub const fn strong_motor(intensity: f32) -> Self {
Self {
strong_motor: intensity,
weak_motor: 0.0,
}
}
}
/// An event that controls force-feedback rumbling of a [`Gamepad`]
///
/// # Notes
///
/// Does nothing if the gamepad or platform does not support rumble.
///
/// # Example
///
/// ```
/// # use bevy_input::gamepad::{Gamepad, Gamepads, GamepadRumbleRequest, GamepadRumbleIntensity};
/// # use bevy_ecs::prelude::{EventWriter, Res};
/// # use bevy_utils::Duration;
/// fn rumble_gamepad_system(
/// mut rumble_requests: EventWriter<GamepadRumbleRequest>,
/// gamepads: Res<Gamepads>
/// ) {
/// for gamepad in gamepads.iter() {
/// rumble_requests.send(GamepadRumbleRequest::Add {
/// gamepad,
/// intensity: GamepadRumbleIntensity::MAX,
/// duration: Duration::from_secs_f32(0.5),
/// });
/// }
/// }
/// ```
#[doc(alias = "haptic feedback")]
#[doc(alias = "force feedback")]
#[doc(alias = "vibration")]
#[doc(alias = "vibrate")]
#[derive(Clone)]
pub enum GamepadRumbleRequest {
/// Add a rumble to the given gamepad.
///
/// Simultaneous rumble effects add up to the sum of their strengths.
///
/// Consequently, if two rumbles at half intensity are added at the same
/// time, their intensities will be added up, and the controller will rumble
/// at full intensity until one of the rumbles finishes, then the rumble
/// will continue at the intensity of the remaining event.
///
/// To replace an existing rumble, send a [`GamepadRumbleRequest::Stop`] event first.
Add {
/// How long the gamepad should rumble
duration: Duration,
/// How intense the rumble should be
intensity: GamepadRumbleIntensity,
/// The gamepad to rumble
gamepad: Gamepad,
},
/// Stop all running rumbles on the given [`Gamepad`]
Stop { gamepad: Gamepad },
}
impl GamepadRumbleRequest {
/// Get the [`Gamepad`] associated with this request
pub fn gamepad(&self) -> Gamepad {
match self {
Self::Add { gamepad, .. } | Self::Stop { gamepad } => *gamepad,
}
}
}
#[cfg(test)]
mod tests {
use crate::gamepad::{AxisSettingsError, ButtonSettingsError};

View file

@ -39,8 +39,8 @@ use gamepad::{
gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system,
gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis,
GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent,
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadSettings,
Gamepads,
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
GamepadRumbleRequest, GamepadSettings, Gamepads,
};
#[cfg(feature = "serialize")]
@ -72,6 +72,7 @@ impl Plugin for InputPlugin {
.add_event::<GamepadButtonChangedEvent>()
.add_event::<GamepadAxisChangedEvent>()
.add_event::<GamepadEvent>()
.add_event::<GamepadRumbleRequest>()
.init_resource::<GamepadSettings>()
.init_resource::<Gamepads>()
.init_resource::<Input<GamepadButton>>()

View file

@ -21,6 +21,7 @@ trace = [
]
trace_chrome = [ "bevy_log/tracing-chrome" ]
trace_tracy = ["bevy_render?/tracing-tracy", "bevy_log/tracing-tracy" ]
trace_tracy_memory = ["bevy_log/trace_tracy_memory"]
wgpu_trace = ["bevy_render/wgpu_trace"]
debug_asset_server = ["bevy_asset/debug_asset_server"]
detailed_trace = ["bevy_utils/detailed_trace"]
@ -96,6 +97,8 @@ bevy_render = ["dep:bevy_render", "bevy_scene?/bevy_render"]
# Enable assertions to check the validity of parameters passed to glam
glam_assert = ["bevy_math/glam_assert"]
default_font = ["bevy_text?/default_font"]
[dependencies]
# bevy
bevy_a11y = { path = "../bevy_a11y", version = "0.11.0-dev" }

View file

@ -10,6 +10,7 @@ keywords = ["bevy"]
[features]
trace = [ "tracing-error" ]
trace_tracy_memory = ["dep:tracy-client"]
[dependencies]
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
@ -21,6 +22,7 @@ tracing-chrome = { version = "0.7.0", optional = true }
tracing-tracy = { version = "0.10.0", optional = true }
tracing-log = "0.1.2"
tracing-error = { version = "0.2.0", optional = true }
tracy-client = { version = "0.15", optional = true }
[target.'cfg(target_os = "android")'.dependencies]
android_log-sys = "0.2.0"

View file

@ -18,6 +18,11 @@ use std::panic;
#[cfg(target_os = "android")]
mod android_tracing;
#[cfg(feature = "trace_tracy_memory")]
#[global_allocator]
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
pub mod prelude {
//! The Bevy Log Prelude.
#[doc(hidden)]

View file

@ -160,6 +160,8 @@ pub fn ensure_no_collision(value: Ident, haystack: TokenStream) -> Ident {
/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label trait
/// - `trait_path`: The path [`syn::Path`] to the label trait
pub fn derive_boxed_label(input: syn::DeriveInput, trait_path: &syn::Path) -> TokenStream {
let bevy_utils_path = BevyManifest::default().get_path("bevy_utils");
let ident = input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
@ -178,6 +180,22 @@ pub fn derive_boxed_label(input: syn::DeriveInput, trait_path: &syn::Path) -> To
fn dyn_clone(&self) -> std::boxed::Box<dyn #trait_path> {
std::boxed::Box::new(std::clone::Clone::clone(self))
}
fn as_dyn_eq(&self) -> &dyn #bevy_utils_path::label::DynEq {
self
}
fn dyn_hash(&self, mut state: &mut dyn ::std::hash::Hasher) {
let ty_id = #trait_path::inner_type_id(self);
::std::hash::Hash::hash(&ty_id, &mut state);
::std::hash::Hash::hash(self, &mut state);
}
}
impl #impl_generics ::std::convert::AsRef<dyn #trait_path> for #ident #ty_generics #where_clause {
fn as_ref(&self) -> &dyn #trait_path {
self
}
}
})
.into()

View file

@ -2040,6 +2040,18 @@ pub fn check_light_mesh_visibility(
frustum_visible_entities.entities.push(entity);
}
}
} else {
computed_visibility.set_visible_in_view();
for view in frusta.frusta.keys() {
let view_visible_entities = visible_entities
.entities
.get_mut(view)
.expect("Per-view visible entities should have been inserted already");
for frustum_visible_entities in view_visible_entities {
frustum_visible_entities.entities.push(entity);
}
}
}
}

View file

@ -84,6 +84,10 @@ fn vertex(vertex: Vertex) -> VertexOutput {
#ifdef PREPASS_FRAGMENT
struct FragmentInput {
#ifdef VERTEX_UVS
@location(0) uv: vec2<f32>,
#endif // VERTEX_UVS
#ifdef NORMAL_PREPASS
@location(1) world_normal: vec3<f32>,
#endif // NORMAL_PREPASS

View file

@ -1231,8 +1231,8 @@ impl<P: PhaseItem> RenderCommand<P> for DrawMesh {
pass.set_index_buffer(buffer.slice(..), 0, *index_format);
pass.draw_indexed(0..*count, 0, 0..1);
}
GpuBufferInfo::NonIndexed { vertex_count } => {
pass.draw(0..*vertex_count, 0..1);
GpuBufferInfo::NonIndexed => {
pass.draw(0..gpu_mesh.vertex_count, 0..1);
}
}
RenderCommandResult::Success

View file

@ -23,8 +23,8 @@ struct FragmentInput {
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
let is_orthographic = view.projection[3].w == 1.0;
let V = calculate_view(in.world_position, is_orthographic);
var uv = in.uv;
#ifdef VERTEX_UVS
var uv = in.uv;
#ifdef VERTEX_TANGENTS
if ((material.flags & STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) {
let N = in.world_normal;

View file

@ -148,7 +148,7 @@ impl<'a, A: IsAligned> Ptr<'a, A> {
/// - If the `A` type parameter is [`Aligned`] then `inner` must be sufficiently aligned for the pointee type.
/// - `inner` must have correct provenance to allow reads of the pointee type.
/// - The lifetime `'a` must be constrained such that this [`Ptr`] will stay valid and nothing
/// can mutate the pointee while this [`Ptr`] is live except through an `UnsafeCell`.
/// can mutate the pointee while this [`Ptr`] is live except through an [`UnsafeCell`].
#[inline]
pub unsafe fn new(inner: NonNull<u8>) -> Self {
Self(inner, PhantomData)
@ -167,7 +167,7 @@ impl<'a, A: IsAligned> Ptr<'a, A> {
///
/// # Safety
/// - `T` must be the erased pointee type for this [`Ptr`].
/// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned
/// - If the type parameter `A` is [`Unaligned`] then this pointer must be sufficiently aligned
/// for the pointee type `T`.
#[inline]
pub unsafe fn deref<T>(self) -> &'a T {
@ -290,7 +290,7 @@ impl<'a, A: IsAligned> OwningPtr<'a, A> {
///
/// # Safety
/// - `T` must be the erased pointee type for this [`OwningPtr`].
/// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned
/// - If the type parameter `A` is [`Unaligned`] then this pointer must be sufficiently aligned
/// for the pointee type `T`.
#[inline]
pub unsafe fn read<T>(self) -> T {
@ -301,7 +301,7 @@ impl<'a, A: IsAligned> OwningPtr<'a, A> {
///
/// # Safety
/// - `T` must be the erased pointee type for this [`OwningPtr`].
/// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned
/// - If the type parameter `A` is [`Unaligned`] then this pointer must be sufficiently aligned
/// for the pointee type `T`.
#[inline]
pub unsafe fn drop_as<T>(self) {

View file

@ -32,7 +32,7 @@ pub(crate) struct ResultSifter<T> {
errors: Option<syn::Error>,
}
/// Returns a `Member` made of `ident` or `index` if `ident` is None.
/// Returns a [`Member`] made of `ident` or `index` if `ident` is None.
///
/// Rust struct syntax allows for `Struct { foo: "string" }` with explicitly
/// named fields. It allows the `Struct { 0: "string" }` syntax when the struct

View file

@ -399,7 +399,10 @@ macro_rules! impl_reflect_for_hashmap {
let mut dynamic_map = DynamicMap::default();
dynamic_map.set_name(self.type_name().to_string());
for (k, v) in self {
dynamic_map.insert_boxed(k.clone_value(), v.clone_value());
let key = K::from_reflect(k).unwrap_or_else(|| {
panic!("Attempted to clone invalid key of type {}.", k.type_name())
});
dynamic_map.insert_boxed(Box::new(key), v.clone_value());
}
dynamic_map
}

View file

@ -131,7 +131,7 @@ impl TypeRegistry {
///
/// Most of the time [`TypeRegistry::register`] can be used instead to register a type you derived [`Reflect`] for.
/// However, in cases where you want to add a piece of type data that was not included in the list of `#[reflect(...)]` type data in the derive,
/// or where the type is generic and cannot register e.g. `ReflectSerialize` unconditionally without knowing the specific type parameters,
/// or where the type is generic and cannot register e.g. [`ReflectSerialize`] unconditionally without knowing the specific type parameters,
/// this method can be used to insert additional type data.
///
/// # Example
@ -220,7 +220,7 @@ impl TypeRegistry {
.and_then(|id| self.registrations.get_mut(id))
}
/// Returns a reference to the [`TypeData`] of type `T` associated with the given `TypeId`.
/// Returns a reference to the [`TypeData`] of type `T` associated with the given [`TypeId`].
///
/// The returned value may be used to downcast [`Reflect`] trait objects to
/// trait objects of the trait used to generate `T`, provided that the
@ -234,7 +234,7 @@ impl TypeRegistry {
.and_then(|registration| registration.data::<T>())
}
/// Returns a mutable reference to the [`TypeData`] of type `T` associated with the given `TypeId`.
/// Returns a mutable reference to the [`TypeData`] of type `T` associated with the given [`TypeId`].
///
/// If the specified type has not been registered, or if `T` is not present
/// in its type registration, returns `None`.
@ -243,7 +243,7 @@ impl TypeRegistry {
.and_then(|registration| registration.data_mut::<T>())
}
/// Returns the [`TypeInfo`] associated with the given `TypeId`.
/// Returns the [`TypeInfo`] associated with the given [`TypeId`].
///
/// If the specified type has not been registered, returns `None`.
pub fn get_type_info(&self, type_id: TypeId) -> Option<&'static TypeInfo> {

View file

@ -2,7 +2,7 @@ error[E0599]: no method named `get_field` found for struct `Box<(dyn Reflect + '
--> tests/reflect_derive/generics.fail.rs:14:9
|
14 | foo.get_field::<NoReflect>("a").unwrap();
| ^^^^^^^^^ method not found in `Box<(dyn Reflect + 'static)>`
| ^^^^^^^^^ method not found in `Box<dyn Reflect>`
error[E0277]: the trait bound `NoReflect: Reflect` is not satisfied
--> tests/reflect_derive/generics.fail.rs:12:37

View file

@ -469,7 +469,7 @@ struct UniformBindingMeta {
/// Represents the arguments for any general binding attribute.
///
/// If parsed, represents an attribute
/// like `#[foo(LitInt, ...)]` where the rest is optional `NestedMeta`.
/// like `#[foo(LitInt, ...)]` where the rest is optional [`NestedMeta`].
enum BindingMeta {
IndexOnly(LitInt),
IndexWithOptions(BindingIndexOptions),

View file

@ -18,7 +18,7 @@ use bevy_ecs::{
system::{Commands, Query, Res, ResMut, Resource},
};
use bevy_log::warn;
use bevy_math::{Mat4, Ray, UVec2, UVec4, Vec2, Vec3};
use bevy_math::{Mat4, Ray, Rect, UVec2, UVec4, Vec2, Vec3};
use bevy_reflect::prelude::*;
use bevy_reflect::FromReflect;
use bevy_transform::components::GlobalTransform;
@ -156,13 +156,16 @@ impl Camera {
Some((min, max))
}
/// The rendered logical bounds (minimum, maximum) of the camera. If the `viewport` field is set
/// to [`Some`], this will be the rect of that custom viewport. Otherwise it will default to the
/// The rendered logical bounds [`Rect`] of the camera. If the `viewport` field is set to
/// [`Some`], this will be the rect of that custom viewport. Otherwise it will default to the
/// full logical rect of the current [`RenderTarget`].
#[inline]
pub fn logical_viewport_rect(&self) -> Option<(Vec2, Vec2)> {
pub fn logical_viewport_rect(&self) -> Option<Rect> {
let (min, max) = self.physical_viewport_rect()?;
Some((self.to_logical(min)?, self.to_logical(max)?))
Some(Rect {
min: self.to_logical(min)?,
max: self.to_logical(max)?,
})
}
/// The logical size of this camera's viewport. If the `viewport` field is set to [`Some`], this
@ -423,7 +426,7 @@ impl NormalizedRenderTarget {
match self {
NormalizedRenderTarget::Window(window_ref) => windows
.get(&window_ref.entity())
.and_then(|window| window.swap_chain_texture.as_ref()),
.and_then(|window| window.swap_chain_texture_view.as_ref()),
NormalizedRenderTarget::Image(image_handle) => {
images.get(image_handle).map(|image| &image.texture_view)
}

View file

@ -52,7 +52,7 @@ impl Node for CameraDriverNode {
continue;
}
let Some(swap_chain_texture) = &window.swap_chain_texture else {
let Some(swap_chain_texture) = &window.swap_chain_texture_view else {
continue;
};

View file

@ -79,27 +79,27 @@ pub enum RenderSet {
ExtractCommands,
/// Prepare render resources from the extracted data for the GPU.
Prepare,
/// The copy of [`apply_system_buffers`] that runs immediately after `Prepare`.
/// The copy of [`apply_system_buffers`] that runs immediately after [`Prepare`](RenderSet::Prepare).
PrepareFlush,
/// Create [`BindGroups`](crate::render_resource::BindGroup) that depend on
/// Create [`BindGroups`](render_resource::BindGroup) that depend on
/// [`Prepare`](RenderSet::Prepare) data and queue up draw calls to run during the
/// [`Render`](RenderSet::Render) step.
Queue,
/// The copy of [`apply_system_buffers`] that runs immediately after `Queue`.
/// The copy of [`apply_system_buffers`] that runs immediately after [`Queue`](RenderSet::Queue).
QueueFlush,
// TODO: This could probably be moved in favor of a system ordering abstraction in Render or Queue
/// Sort the [`RenderPhases`](crate::render_phase::RenderPhase) here.
/// Sort the [`RenderPhases`](render_phase::RenderPhase) here.
PhaseSort,
/// The copy of [`apply_system_buffers`] that runs immediately after `PhaseSort`.
/// The copy of [`apply_system_buffers`] that runs immediately after [`PhaseSort`](RenderSet::PhaseSort).
PhaseSortFlush,
/// Actual rendering happens here.
/// In most cases, only the render backend should insert resources here.
Render,
/// The copy of [`apply_system_buffers`] that runs immediately after `Render`.
/// The copy of [`apply_system_buffers`] that runs immediately after [`Render`](RenderSet::Render).
RenderFlush,
/// Cleanup render resources here.
Cleanup,
/// The copy of [`apply_system_buffers`] that runs immediately after `Cleanup`.
/// The copy of [`apply_system_buffers`] that runs immediately after [`Cleanup`](RenderSet::Cleanup).
CleanupFlush,
}

View file

@ -260,7 +260,7 @@ impl Mesh {
}
/// Computes and returns the vertex data of the mesh as bytes.
/// Therefore the attributes are located in alphabetical order.
/// Therefore the attributes are located in the order of their [`MeshVertexAttribute::id`].
/// This is used to transform the vertex data into a GPU friendly format.
///
/// # Panics
@ -820,6 +820,7 @@ impl From<&Indices> for IndexFormat {
pub struct GpuMesh {
/// Contains all attribute data for each vertex.
pub vertex_buffer: Buffer,
pub vertex_count: u32,
pub buffer_info: GpuBufferInfo,
pub primitive_topology: PrimitiveTopology,
pub layout: MeshVertexBufferLayout,
@ -834,9 +835,7 @@ pub enum GpuBufferInfo {
count: u32,
index_format: IndexFormat,
},
NonIndexed {
vertex_count: u32,
},
NonIndexed,
}
impl RenderAsset for Mesh {
@ -861,11 +860,8 @@ impl RenderAsset for Mesh {
contents: &vertex_buffer_data,
});
let buffer_info = mesh.get_index_buffer_bytes().map_or(
GpuBufferInfo::NonIndexed {
vertex_count: mesh.count_vertices() as u32,
},
|data| GpuBufferInfo::Indexed {
let buffer_info = if let Some(data) = mesh.get_index_buffer_bytes() {
GpuBufferInfo::Indexed {
buffer: render_device.create_buffer_with_data(&BufferInitDescriptor {
usage: BufferUsages::INDEX,
contents: data,
@ -873,13 +869,16 @@ impl RenderAsset for Mesh {
}),
count: mesh.indices().unwrap().len() as u32,
index_format: mesh.indices().unwrap().into(),
},
);
}
} else {
GpuBufferInfo::NonIndexed
};
let mesh_vertex_buffer_layout = mesh.get_mesh_vertex_buffer_layout();
Ok(GpuMesh {
vertex_buffer,
vertex_count: mesh.count_vertices() as u32,
buffer_info,
primitive_topology: mesh.primitive_topology(),
layout: mesh_vertex_buffer_layout,

View file

@ -6,13 +6,16 @@ use super::NodeId;
/// They are used to describe the ordering (which node has to run first)
/// and may be of two kinds: [`NodeEdge`](Self::NodeEdge) and [`SlotEdge`](Self::SlotEdge).
///
/// Edges are added via the `render_graph::add_node_edge(output_node, input_node)` and the
/// `render_graph::add_slot_edge(output_node, output_slot, input_node, input_slot)` methods.
/// Edges are added via the [`RenderGraph::add_node_edge`] and the
/// [`RenderGraph::add_slot_edge`] methods.
///
/// The former simply states that the `output_node` has to be run before the `input_node`,
/// while the later connects an output slot of the `output_node`
/// with an input slot of the `input_node` to pass additional data along.
/// For more information see [`SlotType`](super::SlotType).
///
/// [`RenderGraph::add_node_edge`]: crate::render_graph::RenderGraph::add_node_edge
/// [`RenderGraph::add_slot_edge`]: crate::render_graph::RenderGraph::add_slot_edge
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Edge {
/// An edge describing to ordering of both nodes (`output_node` before `input_node`)

View file

@ -18,7 +18,7 @@ use std::{any::TypeId, fmt::Debug, hash::Hash};
/// [`RenderCommand`]s. For more details and an example see the [`RenderCommand`] documentation.
pub trait Draw<P: PhaseItem>: Send + Sync + 'static {
/// Prepares the draw function to be used. This is called once and only once before the phase
/// begins. There may be zero or more `draw` calls following a call to this function.
/// begins. There may be zero or more [`draw`](Draw::draw) calls following a call to this function.
/// Implementing this is optional.
#[allow(unused_variables)]
fn prepare(&mut self, world: &'_ World) {}
@ -249,7 +249,7 @@ where
C::Param: ReadOnlySystemParam,
{
/// Prepares the render command to be used. This is called once and only once before the phase
/// begins. There may be zero or more `draw` calls following a call to this function.
/// begins. There may be zero or more [`draw`](RenderCommandState::draw) calls following a call to this function.
fn prepare(&mut self, world: &'_ World) {
self.state.update_archetypes(world);
self.view.update_archetypes(world);

View file

@ -51,31 +51,21 @@ define_atomic_id!(TextureViewId);
render_resource_wrapper!(ErasedTextureView, wgpu::TextureView);
render_resource_wrapper!(ErasedSurfaceTexture, wgpu::SurfaceTexture);
/// This type combines wgpu's [`TextureView`](wgpu::TextureView) and
/// [`SurfaceTexture`](wgpu::SurfaceTexture) into the same interface.
#[derive(Clone, Debug)]
pub enum TextureViewValue {
/// The value is an actual wgpu [`TextureView`](wgpu::TextureView).
TextureView(ErasedTextureView),
/// The value is a wgpu [`SurfaceTexture`](wgpu::SurfaceTexture), but dereferences to
/// a [`TextureView`](wgpu::TextureView).
SurfaceTexture {
// NOTE: The order of these fields is important because the view must be dropped before the
// frame is dropped
view: ErasedTextureView,
texture: ErasedSurfaceTexture,
},
}
/// Describes a [`Texture`] with its associated metadata required by a pipeline or [`BindGroup`](super::BindGroup).
///
/// May be converted from a [`TextureView`](wgpu::TextureView) or [`SurfaceTexture`](wgpu::SurfaceTexture)
/// or dereferences to a wgpu [`TextureView`](wgpu::TextureView).
#[derive(Clone, Debug)]
pub struct TextureView {
id: TextureViewId,
value: TextureViewValue,
value: ErasedTextureView,
}
pub struct SurfaceTexture {
value: ErasedSurfaceTexture,
}
impl SurfaceTexture {
pub fn try_unwrap(self) -> Option<wgpu::SurfaceTexture> {
self.value.try_unwrap()
}
}
impl TextureView {
@ -84,34 +74,21 @@ impl TextureView {
pub fn id(&self) -> TextureViewId {
self.id
}
/// Returns the [`SurfaceTexture`](wgpu::SurfaceTexture) of the texture view if it is of that type.
#[inline]
pub fn take_surface_texture(self) -> Option<wgpu::SurfaceTexture> {
match self.value {
TextureViewValue::TextureView(_) => None,
TextureViewValue::SurfaceTexture { texture, .. } => texture.try_unwrap(),
}
}
}
impl From<wgpu::TextureView> for TextureView {
fn from(value: wgpu::TextureView) -> Self {
TextureView {
id: TextureViewId::new(),
value: TextureViewValue::TextureView(ErasedTextureView::new(value)),
value: ErasedTextureView::new(value),
}
}
}
impl From<wgpu::SurfaceTexture> for TextureView {
impl From<wgpu::SurfaceTexture> for SurfaceTexture {
fn from(value: wgpu::SurfaceTexture) -> Self {
let view = ErasedTextureView::new(value.texture.create_view(&Default::default()));
let texture = ErasedSurfaceTexture::new(value);
TextureView {
id: TextureViewId::new(),
value: TextureViewValue::SurfaceTexture { texture, view },
SurfaceTexture {
value: ErasedSurfaceTexture::new(value),
}
}
}
@ -121,10 +98,16 @@ impl Deref for TextureView {
#[inline]
fn deref(&self) -> &Self::Target {
match &self.value {
TextureViewValue::TextureView(value) => value,
TextureViewValue::SurfaceTexture { view, .. } => view,
}
&self.value
}
}
impl Deref for SurfaceTexture {
type Target = wgpu::SurfaceTexture;
#[inline]
fn deref(&self) -> &Self::Target {
&self.value
}
}

View file

@ -57,9 +57,12 @@ impl RenderGraphRunner {
render_device: RenderDevice,
queue: &wgpu::Queue,
world: &World,
finalizer: impl FnOnce(&mut wgpu::CommandEncoder),
) -> Result<(), RenderGraphRunnerError> {
let mut render_context = RenderContext::new(render_device);
Self::run_graph(graph, None, &mut render_context, world, &[], None)?;
finalizer(render_context.command_encoder());
{
#[cfg(feature = "trace")]
let _span = info_span!("submit_graph_commands").entered();

View file

@ -35,6 +35,9 @@ pub fn render_system(world: &mut World) {
render_device.clone(), // TODO: is this clone really necessary?
&render_queue.0,
world,
|encoder| {
crate::view::screenshot::submit_screenshot_commands(world, encoder);
},
) {
error!("Error running render graph:");
{
@ -66,8 +69,8 @@ pub fn render_system(world: &mut World) {
let mut windows = world.resource_mut::<ExtractedWindows>();
for window in windows.values_mut() {
if let Some(texture_view) = window.swap_chain_texture.take() {
if let Some(surface_texture) = texture_view.take_surface_texture() {
if let Some(wrapped_texture) = window.swap_chain_texture.take() {
if let Some(surface_texture) = wrapped_texture.try_unwrap() {
surface_texture.present();
}
}
@ -81,6 +84,8 @@ pub fn render_system(world: &mut World) {
);
}
crate::view::screenshot::collect_screenshots(world);
// update the time and send it to the app world
let time_sender = world.resource::<TimeSender>();
time_sender.0.try_send(Instant::now()).expect(
@ -102,7 +107,7 @@ pub struct RenderAdapter(pub Arc<Adapter>);
#[derive(Resource, Deref, DerefMut)]
pub struct RenderInstance(pub Instance);
/// The `AdapterInfo` of the adapter in use by the renderer.
/// The [`AdapterInfo`] of the adapter in use by the renderer.
#[derive(Resource, Clone, Deref, DerefMut)]
pub struct RenderAdapterInfo(pub AdapterInfo);

View file

@ -128,19 +128,19 @@ pub enum ImageSampler {
}
impl ImageSampler {
/// Returns an image sampler with `Linear` min and mag filters
/// Returns an image sampler with [`Linear`](crate::render_resource::FilterMode::Linear) min and mag filters
#[inline]
pub fn linear() -> ImageSampler {
ImageSampler::Descriptor(Self::linear_descriptor())
}
/// Returns an image sampler with `nearest` min and mag filters
/// Returns an image sampler with [`Nearest`](crate::render_resource::FilterMode::Nearest) min and mag filters
#[inline]
pub fn nearest() -> ImageSampler {
ImageSampler::Descriptor(Self::nearest_descriptor())
}
/// Returns a sampler descriptor with `Linear` min and mag filters
/// Returns a sampler descriptor with [`Linear`](crate::render_resource::FilterMode::Linear) min and mag filters
#[inline]
pub fn linear_descriptor() -> wgpu::SamplerDescriptor<'static> {
wgpu::SamplerDescriptor {
@ -151,7 +151,7 @@ impl ImageSampler {
}
}
/// Returns a sampler descriptor with `Nearest` min and mag filters
/// Returns a sampler descriptor with [`Nearest`](crate::render_resource::FilterMode::Nearest) min and mag filters
#[inline]
pub fn nearest_descriptor() -> wgpu::SamplerDescriptor<'static> {
wgpu::SamplerDescriptor {
@ -230,7 +230,6 @@ impl Image {
///
/// # Panics
/// Panics if the size of the `format` is not a multiple of the length of the `pixel` data.
/// do not match.
pub fn new_fill(
size: Extent3d,
dimension: TextureDimension,

View file

@ -174,6 +174,7 @@ impl Image {
/// - `TextureFormat::R8Unorm`
/// - `TextureFormat::Rg8Unorm`
/// - `TextureFormat::Rgba8UnormSrgb`
/// - `TextureFormat::Bgra8UnormSrgb`
///
/// To convert [`Image`] to a different format see: [`Image::convert`].
pub fn try_into_dynamic(self) -> anyhow::Result<DynamicImage> {
@ -196,6 +197,20 @@ impl Image {
self.data,
)
.map(DynamicImage::ImageRgba8),
// This format is commonly used as the format for the swapchain texture
// This conversion is added here to support screenshots
TextureFormat::Bgra8UnormSrgb => ImageBuffer::from_raw(
self.texture_descriptor.size.width,
self.texture_descriptor.size.height,
{
let mut data = self.data;
for bgra in data.chunks_exact_mut(4) {
bgra.swap(0, 2);
}
data
},
)
.map(DynamicImage::ImageRgba8),
// Throw and error if conversion isn't supported
texture_format => {
return Err(anyhow!(

View file

@ -25,7 +25,7 @@ use crate::{
/// User indication of whether an entity is visible. Propagates down the entity hierarchy.
///
/// If an entity is hidden in this way, all [`Children`] (and all of their children and so on) who
/// are set to `Inherited` will also be hidden.
/// are set to [`Inherited`](Self::Inherited) will also be hidden.
///
/// This is done by the `visibility_propagate_system` which uses the entity hierarchy and
/// `Visibility` to set the values of each entity's [`ComputedVisibility`] component.

View file

@ -1,6 +1,7 @@
use crate::{
render_resource::TextureView,
render_resource::{PipelineCache, SpecializedRenderPipelines, SurfaceTexture, TextureView},
renderer::{RenderAdapter, RenderDevice, RenderInstance},
texture::TextureFormatPixelInfo,
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_app::{App, Plugin};
@ -10,7 +11,13 @@ use bevy_window::{
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed,
};
use std::ops::{Deref, DerefMut};
use wgpu::TextureFormat;
use wgpu::{BufferUsages, TextureFormat, TextureUsages};
pub mod screenshot;
use screenshot::{
ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
};
use super::Msaa;
@ -27,10 +34,13 @@ pub enum WindowSystem {
impl Plugin for WindowRenderPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(ScreenshotPlugin);
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<ExtractedWindows>()
.init_resource::<WindowSurfaces>()
.init_resource::<ScreenshotToScreenPipeline>()
.init_non_send_resource::<NonSendMarker>()
.add_systems(ExtractSchedule, extract_windows)
.configure_set(Render, WindowSystem::Prepare.in_set(RenderSet::Prepare))
@ -46,11 +56,26 @@ pub struct ExtractedWindow {
pub physical_width: u32,
pub physical_height: u32,
pub present_mode: PresentMode,
pub swap_chain_texture: Option<TextureView>,
/// Note: this will not always be the swap chain texture view. When taking a screenshot,
/// this will point to an alternative texture instead to allow for copying the render result
/// to CPU memory.
pub swap_chain_texture_view: Option<TextureView>,
pub swap_chain_texture: Option<SurfaceTexture>,
pub swap_chain_texture_format: Option<TextureFormat>,
pub screenshot_memory: Option<ScreenshotPreparedState>,
pub size_changed: bool,
pub present_mode_changed: bool,
pub alpha_mode: CompositeAlphaMode,
pub screenshot_func: Option<screenshot::ScreenshotFn>,
}
impl ExtractedWindow {
fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {
self.swap_chain_texture_view = Some(TextureView::from(
frame.texture.create_view(&Default::default()),
));
self.swap_chain_texture = Some(SurfaceTexture::from(frame));
}
}
#[derive(Default, Resource)]
@ -75,6 +100,7 @@ impl DerefMut for ExtractedWindows {
fn extract_windows(
mut extracted_windows: ResMut<ExtractedWindows>,
screenshot_manager: Extract<Res<ScreenshotManager>>,
mut closed: Extract<EventReader<WindowClosed>>,
windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
) {
@ -95,14 +121,17 @@ fn extract_windows(
physical_height: new_height,
present_mode: window.present_mode,
swap_chain_texture: None,
swap_chain_texture_view: None,
size_changed: false,
swap_chain_texture_format: None,
present_mode_changed: false,
alpha_mode: window.composite_alpha_mode,
screenshot_func: None,
screenshot_memory: None,
});
// NOTE: Drop the swap chain frame here
extracted_window.swap_chain_texture = None;
extracted_window.swap_chain_texture_view = None;
extracted_window.size_changed = new_width != extracted_window.physical_width
|| new_height != extracted_window.physical_height;
extracted_window.present_mode_changed =
@ -132,6 +161,15 @@ fn extract_windows(
for closed_window in closed.iter() {
extracted_windows.remove(&closed_window.window);
}
// This lock will never block because `callbacks` is `pub(crate)` and this is the singular callsite where it's locked.
// Even if a user had multiple copies of this system, since the system has a mutable resource access the two systems would never run
// at the same time
// TODO: since this is guaranteed, should the lock be replaced with an UnsafeCell to remove the overhead, or is it minor enough to be ignored?
for (window, screenshot_func) in screenshot_manager.callbacks.lock().drain() {
if let Some(window) = extracted_windows.get_mut(&window) {
window.screenshot_func = Some(screenshot_func);
}
}
}
struct SurfaceData {
@ -167,6 +205,7 @@ pub struct WindowSurfaces {
/// another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and
/// [`Backends::GL`](crate::settings::Backends::GL) if your GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or
/// later.
#[allow(clippy::too_many_arguments)]
pub fn prepare_windows(
// By accessing a NonSend resource, we tell the scheduler to put this system on the main thread,
// which is necessary for some OS s
@ -176,6 +215,9 @@ pub fn prepare_windows(
render_device: Res<RenderDevice>,
render_instance: Res<RenderInstance>,
render_adapter: Res<RenderAdapter>,
screenshot_pipeline: Res<ScreenshotToScreenPipeline>,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>,
mut msaa: ResMut<Msaa>,
) {
for window in windows.windows.values_mut() {
@ -285,18 +327,18 @@ pub fn prepare_windows(
let frame = surface
.get_current_texture()
.expect("Error configuring surface");
window.swap_chain_texture = Some(TextureView::from(frame));
window.set_swapchain_texture(frame);
} else {
match surface.get_current_texture() {
Ok(frame) => {
window.swap_chain_texture = Some(TextureView::from(frame));
window.set_swapchain_texture(frame);
}
Err(wgpu::SurfaceError::Outdated) => {
render_device.configure_surface(surface, &surface_configuration);
let frame = surface
.get_current_texture()
.expect("Error reconfiguring surface");
window.swap_chain_texture = Some(TextureView::from(frame));
window.set_swapchain_texture(frame);
}
#[cfg(target_os = "linux")]
Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => {
@ -311,5 +353,55 @@ pub fn prepare_windows(
}
};
window.swap_chain_texture_format = Some(surface_data.format);
if window.screenshot_func.is_some() {
let texture = render_device.create_texture(&wgpu::TextureDescriptor {
label: Some("screenshot-capture-rendertarget"),
size: wgpu::Extent3d {
width: surface_configuration.width,
height: surface_configuration.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: surface_configuration.format,
usage: TextureUsages::RENDER_ATTACHMENT
| TextureUsages::COPY_SRC
| TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let texture_view = texture.create_view(&Default::default());
let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
label: Some("screenshot-transfer-buffer"),
size: screenshot::get_aligned_size(
window.physical_width,
window.physical_height,
surface_data.format.pixel_size() as u32,
) as u64,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = render_device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("screenshot-to-screen-bind-group"),
layout: &screenshot_pipeline.bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
}],
});
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&screenshot_pipeline,
surface_configuration.format,
);
window.swap_chain_texture_view = Some(texture_view);
window.screenshot_memory = Some(ScreenshotPreparedState {
texture,
buffer,
bind_group,
pipeline_id,
});
}
}
}

View file

@ -0,0 +1,315 @@
use std::{borrow::Cow, num::NonZeroU32, path::Path};
use bevy_app::Plugin;
use bevy_asset::{load_internal_asset, HandleUntyped};
use bevy_ecs::prelude::*;
use bevy_log::{error, info, info_span};
use bevy_reflect::TypeUuid;
use bevy_tasks::AsyncComputeTaskPool;
use bevy_utils::HashMap;
use parking_lot::Mutex;
use thiserror::Error;
use wgpu::{
CommandEncoder, Extent3d, ImageDataLayout, TextureFormat, COPY_BYTES_PER_ROW_ALIGNMENT,
};
use crate::{
prelude::{Image, Shader},
render_resource::{
BindGroup, BindGroupLayout, Buffer, CachedRenderPipelineId, FragmentState, PipelineCache,
RenderPipelineDescriptor, SpecializedRenderPipeline, SpecializedRenderPipelines, Texture,
VertexState,
},
renderer::RenderDevice,
texture::TextureFormatPixelInfo,
RenderApp,
};
use super::ExtractedWindows;
pub type ScreenshotFn = Box<dyn FnOnce(Image) + Send + Sync>;
/// A resource which allows for taking screenshots of the window.
#[derive(Resource, Default)]
pub struct ScreenshotManager {
// this is in a mutex to enable extraction with only an immutable reference
pub(crate) callbacks: Mutex<HashMap<Entity, ScreenshotFn>>,
}
#[derive(Error, Debug)]
#[error("A screenshot for this window has already been requested.")]
pub struct ScreenshotAlreadyRequestedError;
impl ScreenshotManager {
/// Signals the renderer to take a screenshot of this frame.
///
/// The given callback will eventually be called on one of the [`AsyncComputeTaskPool`]s threads.
pub fn take_screenshot(
&mut self,
window: Entity,
callback: impl FnOnce(Image) + Send + Sync + 'static,
) -> Result<(), ScreenshotAlreadyRequestedError> {
self.callbacks
.get_mut()
.try_insert(window, Box::new(callback))
.map(|_| ())
.map_err(|_| ScreenshotAlreadyRequestedError)
}
/// Signals the renderer to take a screenshot of this frame.
///
/// The screenshot will eventually be saved to the given path, and the format will be derived from the extension.
pub fn save_screenshot_to_disk(
&mut self,
window: Entity,
path: impl AsRef<Path>,
) -> Result<(), ScreenshotAlreadyRequestedError> {
let path = path.as_ref().to_owned();
self.take_screenshot(window, move |img| match img.try_into_dynamic() {
Ok(dyn_img) => match image::ImageFormat::from_path(&path) {
Ok(format) => {
// discard the alpha channel which stores brightness values when HDR is enabled to make sure
// the screenshot looks right
let img = dyn_img.to_rgb8();
match img.save_with_format(&path, format) {
Ok(_) => info!("Screenshot saved to {}", path.display()),
Err(e) => error!("Cannot save screenshot, IO error: {e}"),
}
}
Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"),
},
Err(e) => error!("Cannot save screenshot, screen format cannot be understood: {e}"),
})
}
}
pub struct ScreenshotPlugin;
const SCREENSHOT_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 11918575842344596158);
impl Plugin for ScreenshotPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.init_resource::<ScreenshotManager>();
load_internal_asset!(
app,
SCREENSHOT_SHADER_HANDLE,
"screenshot.wgsl",
Shader::from_wgsl
);
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>();
}
}
}
pub(crate) fn align_byte_size(value: u32) -> u32 {
value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT))
}
pub(crate) fn get_aligned_size(width: u32, height: u32, pixel_size: u32) -> u32 {
height * align_byte_size(width * pixel_size)
}
pub(crate) fn layout_data(width: u32, height: u32, format: TextureFormat) -> ImageDataLayout {
ImageDataLayout {
bytes_per_row: if height > 1 {
// 1 = 1 row
NonZeroU32::new(get_aligned_size(width, 1, format.pixel_size() as u32))
} else {
None
},
rows_per_image: None,
..Default::default()
}
}
#[derive(Resource)]
pub struct ScreenshotToScreenPipeline {
pub bind_group_layout: BindGroupLayout,
}
impl FromWorld for ScreenshotToScreenPipeline {
fn from_world(render_world: &mut World) -> Self {
let device = render_world.resource::<RenderDevice>();
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("screenshot-to-screen-bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
Self { bind_group_layout }
}
}
impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {
type Key = TextureFormat;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some(Cow::Borrowed("screenshot-to-screen")),
layout: vec![self.bind_group_layout.clone()],
vertex: VertexState {
buffers: vec![],
shader_defs: vec![],
entry_point: Cow::Borrowed("vs_main"),
shader: SCREENSHOT_SHADER_HANDLE.typed(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
unclipped_depth: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(FragmentState {
shader: SCREENSHOT_SHADER_HANDLE.typed(),
entry_point: Cow::Borrowed("fs_main"),
shader_defs: vec![],
targets: vec![Some(wgpu::ColorTargetState {
format: key,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
push_constant_ranges: Vec::new(),
}
}
}
pub struct ScreenshotPreparedState {
pub texture: Texture,
pub buffer: Buffer,
pub bind_group: BindGroup,
pub pipeline_id: CachedRenderPipelineId,
}
pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) {
let windows = world.resource::<ExtractedWindows>();
let pipelines = world.resource::<PipelineCache>();
for window in windows.values() {
if let Some(memory) = &window.screenshot_memory {
let width = window.physical_width;
let height = window.physical_height;
let texture_format = window.swap_chain_texture_format.unwrap();
encoder.copy_texture_to_buffer(
memory.texture.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer: &memory.buffer,
layout: crate::view::screenshot::layout_data(width, height, texture_format),
},
Extent3d {
width,
height,
..Default::default()
},
);
if let Some(pipeline) = pipelines.get_render_pipeline(memory.pipeline_id) {
let true_swapchain_texture_view = window
.swap_chain_texture
.as_ref()
.unwrap()
.texture
.create_view(&Default::default());
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("screenshot_to_screen_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &true_swapchain_texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &memory.bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
}
}
pub(crate) fn collect_screenshots(world: &mut World) {
let _span = info_span!("collect_screenshots");
let mut windows = world.resource_mut::<ExtractedWindows>();
for window in windows.values_mut() {
if let Some(screenshot_func) = window.screenshot_func.take() {
let width = window.physical_width;
let height = window.physical_height;
let texture_format = window.swap_chain_texture_format.unwrap();
let pixel_size = texture_format.pixel_size();
let ScreenshotPreparedState { buffer, .. } = window.screenshot_memory.take().unwrap();
let finish = async move {
let (tx, rx) = async_channel::bounded(1);
let buffer_slice = buffer.slice(..);
// The polling for this map call is done every frame when the command queue is submitted.
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
let err = result.err();
if err.is_some() {
panic!("{}", err.unwrap().to_string());
}
tx.try_send(()).unwrap();
});
rx.recv().await.unwrap();
let data = buffer_slice.get_mapped_range();
// we immediately move the data to CPU memory to avoid holding the mapped view for long
let mut result = Vec::from(&*data);
drop(data);
drop(buffer);
if result.len() != ((width * height) as usize * pixel_size) {
// Our buffer has been padded because we needed to align to a multiple of 256.
// We remove this padding here
let initial_row_bytes = width as usize * pixel_size;
let buffered_row_bytes = align_byte_size(width * pixel_size as u32) as usize;
let mut take_offset = buffered_row_bytes;
let mut place_offset = initial_row_bytes;
for _ in 1..height {
result.copy_within(
take_offset..take_offset + buffered_row_bytes,
place_offset,
);
take_offset += buffered_row_bytes;
place_offset += initial_row_bytes;
}
result.truncate(initial_row_bytes * height as usize);
}
screenshot_func(Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
wgpu::TextureDimension::D2,
result,
texture_format,
));
};
AsyncComputeTaskPool::get().spawn(finish).detach();
}
}
}

View file

@ -0,0 +1,16 @@
// This vertex shader will create a triangle that will cover the entire screen
// with minimal effort, avoiding the need for a vertex buffer etc.
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = f32((in_vertex_index & 1u) << 2u);
let y = f32((in_vertex_index & 2u) << 1u);
return vec4<f32>(x - 1.0, y - 1.0, 0.0, 1.0);
}
@group(0) @binding(0) var t: texture_2d<f32>;
@fragment
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let coords = floor(pos.xy);
return textureLoad(t, vec2<i32>(coords), 0i);
}

View file

@ -36,10 +36,10 @@ pub struct DynamicScene {
/// A reflection-powered serializable representation of an entity and its components.
pub struct DynamicEntity {
/// The transiently unique identifier of a corresponding `Entity`.
/// The transiently unique identifier of a corresponding [`Entity`](bevy_ecs::entity::Entity).
pub entity: u32,
/// A vector of boxed components that belong to the given entity and
/// implement the `Reflect` trait.
/// implement the [`Reflect`] trait.
pub components: Vec<Box<dyn Reflect>>,
}

View file

@ -14,13 +14,13 @@ pub enum Collision {
// TODO: ideally we can remove this once bevy gets a physics system
/// Axis-aligned bounding box collision with "side" detection
/// * `a_pos` and `b_pos` are the center positions of the rectangles, typically obtained by
/// extracting the `translation` field from a `Transform` component
/// extracting the `translation` field from a [`Transform`](bevy_transform::components::Transform) component
/// * `a_size` and `b_size` are the dimensions (width and height) of the rectangles.
///
/// The return value is the side of `B` that `A` has collided with. `Left` means that
/// `A` collided with `B`'s left side. `Top` means that `A` collided with `B`'s top side.
/// If the collision occurs on multiple sides, the side with the deepest penetration is returned.
/// If all sides are involved, `Inside` is returned.
/// The return value is the side of `B` that `A` has collided with. [`Collision::Left`] means that
/// `A` collided with `B`'s left side. [`Collision::Top`] means that `A` collided with `B`'s top side.
/// If the collision occurs on multiple sides, the side with the shallowest penetration is returned.
/// If all sides are involved, [`Collision::Inside`] is returned.
pub fn collide(a_pos: Vec3, a_size: Vec2, b_pos: Vec3, b_size: Vec2) -> Option<Collision> {
let a_min = a_pos.truncate() - a_size / 2.0;
let a_max = a_pos.truncate() + a_size / 2.0;

View file

@ -29,13 +29,17 @@ pub use texture_atlas::*;
pub use texture_atlas_builder::*;
use bevy_app::prelude::*;
use bevy_asset::{AddAsset, Assets, HandleUntyped};
use bevy_asset::{AddAsset, Assets, Handle, HandleUntyped};
use bevy_core_pipeline::core_2d::Transparent2d;
use bevy_ecs::prelude::*;
use bevy_reflect::TypeUuid;
use bevy_render::{
mesh::Mesh,
primitives::Aabb,
render_phase::AddRenderCommand,
render_resource::{Shader, SpecializedRenderPipelines},
texture::Image,
view::{NoFrustumCulling, VisibilitySystems},
ExtractSchedule, Render, RenderApp, RenderSet,
};
@ -58,10 +62,15 @@ impl Plugin for SpritePlugin {
app.add_asset::<TextureAtlas>()
.register_asset_reflect::<TextureAtlas>()
.register_type::<Sprite>()
.register_type::<TextureAtlasSprite>()
.register_type::<Anchor>()
.register_type::<Mesh2dHandle>()
.add_plugin(Mesh2dRenderPlugin)
.add_plugin(ColorMaterialPlugin);
.add_plugin(ColorMaterialPlugin)
.add_systems(
PostUpdate,
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
);
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
@ -88,3 +97,53 @@ impl Plugin for SpritePlugin {
};
}
}
pub fn calculate_bounds_2d(
mut commands: Commands,
meshes: Res<Assets<Mesh>>,
images: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlas>>,
meshes_without_aabb: Query<(Entity, &Mesh2dHandle), (Without<Aabb>, Without<NoFrustumCulling>)>,
sprites_without_aabb: Query<
(Entity, &Sprite, &Handle<Image>),
(Without<Aabb>, Without<NoFrustumCulling>),
>,
atlases_without_aabb: Query<
(Entity, &TextureAtlasSprite, &Handle<TextureAtlas>),
(Without<Aabb>, Without<NoFrustumCulling>),
>,
) {
for (entity, mesh_handle) in &meshes_without_aabb {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
if let Some(aabb) = mesh.compute_aabb() {
commands.entity(entity).insert(aabb);
}
}
}
for (entity, sprite, texture_handle) in &sprites_without_aabb {
if let Some(size) = sprite
.custom_size
.or_else(|| images.get(texture_handle).map(|image| image.size()))
{
let aabb = Aabb {
center: (-sprite.anchor.as_vec() * size).extend(0.0).into(),
half_extents: (0.5 * size).extend(0.0).into(),
};
commands.entity(entity).insert(aabb);
}
}
for (entity, atlas_sprite, atlas_handle) in &atlases_without_aabb {
if let Some(size) = atlas_sprite.custom_size.or_else(|| {
atlases
.get(atlas_handle)
.and_then(|atlas| atlas.textures.get(atlas_sprite.index))
.map(|rect| (rect.min - rect.max).abs())
}) {
let aabb = Aabb {
center: (-atlas_sprite.anchor.as_vec() * size).extend(0.0).into(),
half_extents: (0.5 * size).extend(0.0).into(),
};
commands.entity(entity).insert(aabb);
}
}
}

View file

@ -599,8 +599,8 @@ impl<P: PhaseItem> RenderCommand<P> for DrawMesh2d {
pass.set_index_buffer(buffer.slice(..), 0, *index_format);
pass.draw_indexed(0..*count, 0, 0..1);
}
GpuBufferInfo::NonIndexed { vertex_count } => {
pass.draw(0..*vertex_count, 0..1);
GpuBufferInfo::NonIndexed => {
pass.draw(0..gpu_mesh.vertex_count, 0..1);
}
}
RenderCommandResult::Success

View file

@ -309,7 +309,7 @@ pub struct ExtractedSprite {
pub rect: Option<Rect>,
/// Change the on-screen size of the sprite
pub custom_size: Option<Vec2>,
/// Handle to the `Image` of this sprite
/// Handle to the [`Image`] of this sprite
/// PERF: storing a `HandleId` instead of `Handle<Image>` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped)
pub image_handle_id: HandleId,
pub flip_x: bool,
@ -396,7 +396,18 @@ pub fn extract_sprites(
continue;
}
if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) {
let rect = Some(texture_atlas.textures[atlas_sprite.index]);
let rect = Some(
*texture_atlas
.textures
.get(atlas_sprite.index)
.unwrap_or_else(|| {
panic!(
"Sprite index {:?} does not exist for texture atlas handle {:?}.",
atlas_sprite.index,
texture_atlas_handle.id(),
)
}),
);
extracted_sprites.sprites.push(ExtractedSprite {
entity,
color: atlas_sprite.color,

View file

@ -1,6 +1,6 @@
use crate::Anchor;
use bevy_asset::Handle;
use bevy_ecs::component::Component;
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::{Rect, Vec2};
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
use bevy_render::{color::Color, texture::Image};
@ -23,7 +23,8 @@ pub struct TextureAtlas {
pub(crate) texture_handles: Option<HashMap<Handle<Image>, usize>>,
}
#[derive(Component, Debug, Clone, Reflect)]
#[derive(Component, Debug, Clone, Reflect, FromReflect)]
#[reflect(Component)]
pub struct TextureAtlasSprite {
/// The tint color used to draw the sprite, defaulting to [`Color::WHITE`]
pub color: Color,

View file

@ -572,7 +572,7 @@ impl Drop for TaskPool {
}
}
/// A `TaskPool` scope for running one or more non-`'static` futures.
/// A [`TaskPool`] scope for running one or more non-`'static` futures.
///
/// For more information, see [`TaskPool::scope`].
#[derive(Debug)]

View file

@ -10,6 +10,7 @@ keywords = ["bevy"]
[features]
subpixel_glyph_atlas = []
default_font = []
[dependencies]
# bevy

Binary file not shown.

View file

@ -26,8 +26,11 @@ pub mod prelude {
}
use bevy_app::prelude::*;
use bevy_asset::AddAsset;
#[cfg(feature = "default_font")]
use bevy_asset::load_internal_binary_asset;
use bevy_asset::{AddAsset, HandleUntyped};
use bevy_ecs::prelude::*;
use bevy_reflect::TypeUuid;
use bevy_render::{camera::CameraUpdateSystem, ExtractSchedule, RenderApp};
use bevy_sprite::SpriteSystem;
use std::num::NonZeroUsize;
@ -67,16 +70,20 @@ pub enum YAxisOrientation {
BottomToTop,
}
pub const DEFAULT_FONT_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Font::TYPE_UUID, 1491772431825224042);
impl Plugin for TextPlugin {
fn build(&self, app: &mut App) {
app.add_asset::<Font>()
.add_asset::<FontAtlasSet>()
.register_type::<Text>()
.register_type::<Text2dBounds>()
.register_type::<TextSection>()
.register_type::<Vec<TextSection>>()
.register_type::<TextStyle>()
.register_type::<Text>()
.register_type::<TextAlignment>()
.register_type::<BreakLineOn>()
.init_asset_loader::<FontLoader>()
.init_resource::<TextSettings>()
.init_resource::<FontAtlasWarning>()
@ -97,5 +104,13 @@ impl Plugin for TextPlugin {
extract_text2d_sprite.after(SpriteSystem::ExtractSprites),
);
}
#[cfg(feature = "default_font")]
load_internal_binary_asset!(
app,
DEFAULT_FONT_HANDLE,
"FiraMono-subset.ttf",
|bytes: &[u8]| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
);
}
}

View file

@ -7,7 +7,7 @@ use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas;
use bevy_utils::HashMap;
use glyph_brush_layout::{FontId, SectionText};
use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText};
use crate::{
error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet,
@ -54,7 +54,7 @@ impl TextPipeline {
font_atlas_warning: &mut FontAtlasWarning,
y_axis_orientation: YAxisOrientation,
) -> Result<TextLayoutInfo, TextError> {
let mut scaled_fonts = Vec::new();
let mut scaled_fonts = Vec::with_capacity(sections.len());
let sections = sections
.iter()
.map(|section| {
@ -92,6 +92,9 @@ impl TextPipeline {
for sg in &section_glyphs {
let scaled_font = scaled_fonts[sg.section_index];
let glyph = &sg.glyph;
// The fonts use a coordinate system increasing upwards so ascent is a positive value
// and descent is negative, but Bevy UI uses a downwards increasing coordinate system,
// so we have to subtract from the baseline position to get the minimum and maximum values.
min_x = min_x.min(glyph.position.x);
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
@ -114,4 +117,140 @@ impl TextPipeline {
Ok(TextLayoutInfo { glyphs, size })
}
pub fn create_text_measure(
&mut self,
fonts: &Assets<Font>,
sections: &[TextSection],
scale_factor: f64,
text_alignment: TextAlignment,
linebreak_behaviour: BreakLineOn,
) -> Result<TextMeasureInfo, TextError> {
let mut auto_fonts = Vec::with_capacity(sections.len());
let mut scaled_fonts = Vec::with_capacity(sections.len());
let sections = sections
.iter()
.enumerate()
.map(|(i, section)| {
let font = fonts
.get(&section.style.font)
.ok_or(TextError::NoSuchFont)?;
let font_size = scale_value(section.style.font_size, scale_factor);
auto_fonts.push(font.font.clone());
let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size);
scaled_fonts.push(px_scale_font);
let section = TextMeasureSection {
font_id: FontId(i),
scale: PxScale::from(font_size),
text: section.value.clone(),
};
Ok(section)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(TextMeasureInfo::new(
auto_fonts,
scaled_fonts,
sections,
text_alignment,
linebreak_behaviour.into(),
))
}
}
#[derive(Debug, Clone)]
pub struct TextMeasureSection {
pub text: String,
pub scale: PxScale,
pub font_id: FontId,
}
#[derive(Debug, Clone)]
pub struct TextMeasureInfo {
pub fonts: Vec<ab_glyph::FontArc>,
pub scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
pub sections: Vec<TextMeasureSection>,
pub text_alignment: TextAlignment,
pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker,
pub min_width_content_size: Vec2,
pub max_width_content_size: Vec2,
}
impl TextMeasureInfo {
fn new(
fonts: Vec<ab_glyph::FontArc>,
scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
sections: Vec<TextMeasureSection>,
text_alignment: TextAlignment,
linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker,
) -> Self {
let mut info = Self {
fonts,
scaled_fonts,
sections,
text_alignment,
linebreak_behaviour,
min_width_content_size: Vec2::ZERO,
max_width_content_size: Vec2::ZERO,
};
let section_texts = info.prepare_section_texts();
let min =
info.compute_size_from_section_texts(&section_texts, Vec2::new(0.0, f32::INFINITY));
let max = info.compute_size_from_section_texts(
&section_texts,
Vec2::new(f32::INFINITY, f32::INFINITY),
);
info.min_width_content_size = min;
info.max_width_content_size = max;
info
}
fn prepare_section_texts(&self) -> Vec<SectionText> {
self.sections
.iter()
.map(|section| SectionText {
font_id: section.font_id,
scale: section.scale,
text: &section.text,
})
.collect::<Vec<_>>()
}
fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 {
let geom = SectionGeometry {
bounds: (bounds.x, bounds.y),
..Default::default()
};
let section_glyphs = glyph_brush_layout::Layout::default()
.h_align(self.text_alignment.into())
.line_breaker(self.linebreak_behaviour)
.calculate_glyphs(&self.fonts, &geom, sections);
let mut min_x: f32 = std::f32::MAX;
let mut min_y: f32 = std::f32::MAX;
let mut max_x: f32 = std::f32::MIN;
let mut max_y: f32 = std::f32::MIN;
for sg in section_glyphs {
let scaled_font = &self.scaled_fonts[sg.section_index];
let glyph = &sg.glyph;
// The fonts use a coordinate system increasing upwards so ascent is a positive value
// and descent is negative, but Bevy UI uses a downwards increasing coordinate system,
// so we have to subtract from the baseline position to get the minimum and maximum values.
min_x = min_x.min(glyph.position.x);
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
max_y = max_y.max(glyph.position.y - scaled_font.descent());
}
Vec2::new(max_x - min_x, max_y - min_y)
}
pub fn compute_size(&self, bounds: Vec2) -> Vec2 {
let sections = self.prepare_section_texts();
self.compute_size_from_section_texts(&sections, bounds)
}
}

View file

@ -5,7 +5,7 @@ use bevy_render::color::Color;
use bevy_utils::default;
use serde::{Deserialize, Serialize};
use crate::Font;
use crate::{Font, DEFAULT_FONT_HANDLE};
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
@ -167,7 +167,7 @@ pub struct TextStyle {
impl Default for TextStyle {
fn default() -> Self {
Self {
font: Default::default(),
font: DEFAULT_FONT_HANDLE.typed(),
font_size: 12.0,
color: Color::WHITE,
}

View file

@ -7,7 +7,7 @@ use bevy_ecs::{
event::EventReader,
prelude::With,
reflect::ReflectComponent,
system::{Commands, Local, Query, Res, ResMut},
system::{Local, Query, Res, ResMut},
};
use bevy_math::{Vec2, Vec3};
use bevy_reflect::Reflect;
@ -29,7 +29,7 @@ use crate::{
/// The maximum width and height of text. The text will wrap according to the specified size.
/// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the
/// specified `TextAlignment`.
/// specified [`TextAlignment`](crate::text::TextAlignment).
///
/// Note: only characters that are completely out of the bounds will be truncated, so this is not a
/// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this
@ -72,6 +72,8 @@ pub struct Text2dBundle {
pub visibility: Visibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
pub computed_visibility: ComputedVisibility,
/// Contains the size of the text and its glyph's position and scale data. Generated via [`TextPipeline::queue_text`]
pub text_layout_info: TextLayoutInfo,
}
pub fn extract_text2d_sprite(
@ -147,7 +149,6 @@ pub fn extract_text2d_sprite(
/// It does not modify or observe existing ones.
#[allow(clippy::too_many_arguments)]
pub fn update_text2d_layout(
mut commands: Commands,
// Text items which should be reprocessed again, generally when the font hasn't loaded yet.
mut queue: Local<HashSet<Entity>>,
mut textures: ResMut<Assets<Image>>,
@ -159,12 +160,7 @@ pub fn update_text2d_layout(
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Entity,
Ref<Text>,
Ref<Text2dBounds>,
Option<&mut TextLayoutInfo>,
)>,
mut text_query: Query<(Entity, Ref<Text>, Ref<Text2dBounds>, &mut TextLayoutInfo)>,
) {
// We need to consume the entire iterator, hence `last`
let factor_changed = scale_factor_changed.iter().last().is_some();
@ -175,7 +171,7 @@ pub fn update_text2d_layout(
.map(|window| window.resolution.scale_factor())
.unwrap_or(1.0);
for (entity, text, bounds, text_layout_info) in &mut text_query {
for (entity, text, bounds, mut text_layout_info) in &mut text_query {
if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) {
let text_bounds = Vec2::new(
scale_value(bounds.size.x, scale_factor),
@ -204,12 +200,7 @@ pub fn update_text2d_layout(
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(info) => match text_layout_info {
Some(mut t) => *t = info,
None => {
commands.entity(entity).insert(info);
}
},
Ok(info) => *text_layout_info = info,
}
}
}

View file

@ -224,10 +224,17 @@ impl Timer {
if self.finished() {
if self.mode == TimerMode::Repeating {
self.times_finished_this_tick =
(self.elapsed().as_nanos() / self.duration().as_nanos()) as u32;
// Duration does not have a modulo
self.set_elapsed(self.elapsed() - self.duration() * self.times_finished_this_tick);
self.times_finished_this_tick = self
.elapsed()
.as_nanos()
.checked_div(self.duration().as_nanos())
.map_or(u32::MAX, |x| x as u32);
self.set_elapsed(
self.elapsed()
.as_nanos()
.checked_rem(self.duration().as_nanos())
.map_or(Duration::ZERO, |x| Duration::from_nanos(x as u64)),
);
} else {
self.times_finished_this_tick = 1;
self.set_elapsed(self.duration());
@ -329,7 +336,11 @@ impl Timer {
/// ```
#[inline]
pub fn percent(&self) -> f32 {
self.elapsed().as_secs_f32() / self.duration().as_secs_f32()
if self.duration == Duration::ZERO {
1.0
} else {
self.elapsed().as_secs_f32() / self.duration().as_secs_f32()
}
}
/// Returns the percentage of the timer remaining time (goes from 1.0 to 0.0).
@ -517,6 +528,26 @@ mod tests {
assert_eq!(t.times_finished_this_tick(), 0);
}
#[test]
fn times_finished_this_tick_repeating_zero_duration() {
let mut t = Timer::from_seconds(0.0, TimerMode::Repeating);
assert_eq!(t.times_finished_this_tick(), 0);
assert_eq!(t.elapsed(), Duration::ZERO);
assert_eq!(t.percent(), 1.0);
t.tick(Duration::from_secs(1));
assert_eq!(t.times_finished_this_tick(), u32::MAX);
assert_eq!(t.elapsed(), Duration::ZERO);
assert_eq!(t.percent(), 1.0);
t.tick(Duration::from_secs(2));
assert_eq!(t.times_finished_this_tick(), u32::MAX);
assert_eq!(t.elapsed(), Duration::ZERO);
assert_eq!(t.percent(), 1.0);
t.reset();
assert_eq!(t.times_finished_this_tick(), 0);
assert_eq!(t.elapsed(), Duration::ZERO);
assert_eq!(t.percent(), 1.0);
}
#[test]
fn times_finished_this_tick_precise() {
let mut t = Timer::from_seconds(0.01, TimerMode::Repeating);

View file

@ -45,7 +45,7 @@ impl Command for AddChildInPlace {
/// You most likely want to use [`BuildChildrenTransformExt::remove_parent_in_place`]
/// method on [`EntityCommands`] instead.
pub struct RemoveParentInPlace {
/// `Entity` whose parent must be removed.
/// [`Entity`] whose parent must be removed.
pub child: Entity,
}
impl Command for RemoveParentInPlace {

View file

@ -111,9 +111,9 @@ impl GlobalTransform {
/// Returns the [`Transform`] `self` would have if it was a child of an entity
/// with the `parent` [`GlobalTransform`].
///
/// This is useful if you want to "reparent" an `Entity`. Say you have an entity
/// `e1` that you want to turn into a child of `e2`, but you want `e1` to keep the
/// same global transform, even after re-parenting. You would use:
/// This is useful if you want to "reparent" an [`Entity`](bevy_ecs::entity::Entity).
/// Say you have an entity `e1` that you want to turn into a child of `e2`,
/// but you want `e1` to keep the same global transform, even after re-parenting. You would use:
///
/// ```rust
/// # use bevy_transform::prelude::{GlobalTransform, Transform};

Some files were not shown because too many files have changed in this diff Show more