diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2410b57d2..28a3c16a5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ env: # If nightly is breaking CI, modify this variable to target a specific nightly version. NIGHTLY_TOOLCHAIN: nightly RUSTFLAGS: "-D warnings" - BINSTALL_VERSION: "v1.12.3" + BINSTALL_VERSION: "v1.12.5" concurrency: group: ${{github.workflow}}-${{github.ref}} @@ -95,7 +95,7 @@ jobs: - name: CI job # To run the tests one item at a time for troubleshooting, use # cargo --quiet test --lib -- --list | sed 's/: test$//' | MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-disable-weak-memory-emulation" xargs -n1 cargo miri test -p bevy_ecs --lib -- --exact - run: cargo miri test -p bevy_ecs + run: cargo miri test -p bevy_ecs --features bevy_utils/debug env: # -Zrandomize-layout makes sure we dont rely on the layout of anything that might change RUSTFLAGS: -Zrandomize-layout @@ -247,7 +247,7 @@ jobs: - name: Check wasm run: cargo check --target wasm32-unknown-unknown -Z build-std=std,panic_abort env: - RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory -D warnings" + RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory" markdownlint: runs-on: ubuntu-latest @@ -272,7 +272,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: cargo-bins/cargo-binstall@v1.12.3 + - uses: cargo-bins/cargo-binstall@v1.12.5 - name: Install taplo run: cargo binstall taplo-cli@0.9.3 --locked - name: Run Taplo diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 91a98f3ea7..485861ebdf 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -49,7 +49,8 @@ jobs: --exclude ci \ --exclude errors \ --exclude bevy_mobile_example \ - --exclude build-wasm-example + --exclude build-wasm-example \ + --exclude no_std_library - name: Create PR uses: peter-evans/create-pull-request@v7 diff --git a/Cargo.toml b/Cargo.toml index 703877983c..87904d32fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" categories = ["game-engines", "graphics", "gui", "rendering"] description = "A refreshingly simple data-driven game engine and app framework" @@ -72,7 +72,6 @@ allow_attributes_without_reason = "warn" [workspace.lints.rust] missing_docs = "warn" -mismatched_lifetime_syntaxes = "allow" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_code = "deny" unsafe_op_in_unsafe_fn = "warn" @@ -134,6 +133,7 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_core_widgets", "bevy_anti_aliasing", "bevy_gilrs", "bevy_gizmos", @@ -164,6 +164,7 @@ default = [ "vorbis", "webgl2", "x11", + "debug", ] # Recommended defaults for no_std applications @@ -246,6 +247,15 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] +# Provides raytraced lighting (experimental) +bevy_solari = [ + "bevy_internal/bevy_solari", + "bevy_asset", + "bevy_core_pipeline", + "bevy_pbr", + "bevy_render", +] + # Provides sprite functionality bevy_sprite = [ "bevy_internal/bevy_sprite", @@ -292,6 +302,9 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] +# Headless widget collection for Bevy UI. +bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] + # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -317,6 +330,9 @@ trace = ["bevy_internal/trace", "dep:tracing"] # Basis Universal compressed texture support basis-universal = ["bevy_internal/basis-universal"] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = ["bevy_internal/compressed_image_saver"] + # BMP image format support bmp = ["bevy_internal/bmp"] @@ -494,7 +510,10 @@ file_watcher = ["bevy_internal/file_watcher"] embedded_watcher = ["bevy_internal/embedded_watcher"] # Enable stepping-based debugging of Bevy systems -bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"] +bevy_debug_stepping = [ + "bevy_internal/bevy_debug_stepping", + "bevy_internal/debug", +] # Enables the meshlet renderer for dense high-poly scenes (experimental) meshlet = ["bevy_internal/meshlet"] @@ -538,30 +557,33 @@ web = ["bevy_internal/web"] # Enable hotpatching of Bevy systems hotpatching = ["bevy_internal/hotpatching"] +# Enable collecting debug information about systems and components to help with diagnostics +debug = ["bevy_internal/debug"] + [dependencies] -bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } +bevy_internal = { path = "crates/bevy_internal", version = "0.17.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } # Wasm does not support dynamic linking. [target.'cfg(not(target_family = "wasm"))'.dependencies] -bevy_dylib = { path = "crates/bevy_dylib", version = "0.16.0-dev", default-features = false, optional = true } +bevy_dylib = { path = "crates/bevy_dylib", version = "0.17.0-dev", default-features = false, optional = true } [dev-dependencies] rand = "0.8.0" rand_chacha = "0.3.1" -ron = "0.8.0" +ron = "0.10" flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" bytemuck = "1.7" -bevy_render = { path = "crates/bevy_render", version = "0.16.0-dev", default-features = false } +bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. -bevy_ecs = { path = "crates/bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_state = { path = "crates/bevy_state", version = "0.16.0-dev", default-features = false } -bevy_asset = { path = "crates/bevy_asset", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "crates/bevy_reflect", version = "0.16.0-dev", default-features = false } -bevy_image = { path = "crates/bevy_image", version = "0.16.0-dev", default-features = false } -bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.16.0-dev", default-features = false } +bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_state = { path = "crates/bevy_state", version = "0.17.0-dev", default-features = false } +bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "crates/bevy_reflect", version = "0.17.0-dev", default-features = false } +bevy_image = { path = "crates/bevy_image", version = "0.17.0-dev", default-features = false } +bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.17.0-dev", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" async-std = "1.13" @@ -588,7 +610,7 @@ web-sys = { version = "0.3", features = ["Window"] } [[example]] name = "context_menu" -path = "examples/usages/context_menu.rs" +path = "examples/usage/context_menu.rs" doc-scrape-examples = true [package.metadata.example.context_menu] @@ -826,6 +848,17 @@ description = "Generates a texture atlas (sprite sheet) from individual sprites" category = "2D Rendering" wasm = false +[[example]] +name = "tilemap_chunk" +path = "examples/2d/tilemap_chunk.rs" +doc-scrape-examples = true + +[package.metadata.example.tilemap_chunk] +name = "Tilemap Chunk" +description = "Renders a tilemap chunk" +category = "2D Rendering" +wasm = true + [[example]] name = "transparency_2d" path = "examples/2d/transparency_2d.rs" @@ -1257,6 +1290,18 @@ description = "Load a cubemap texture onto a cube like a skybox and cycle throug category = "3D Rendering" wasm = false +[[example]] +name = "solari" +path = "examples/3d/solari.rs" +doc-scrape-examples = true +required-features = ["bevy_solari"] + +[package.metadata.example.solari] +name = "Solari" +description = "Demonstrates realtime dynamic raytraced lighting using Bevy Solari." +category = "3D Rendering" +wasm = false # Raytracing is not supported on the web + [[example]] name = "spherical_area_lights" path = "examples/3d/spherical_area_lights.rs" @@ -2073,6 +2118,7 @@ wasm = false name = "dynamic" path = "examples/ecs/dynamic.rs" doc-scrape-examples = true +required-features = ["debug"] [package.metadata.example.dynamic] name = "Dynamic ECS" @@ -4438,3 +4484,25 @@ name = "Hotpatching Systems" description = "Demonstrates how to hotpatch systems" category = "ECS (Entity Component System)" wasm = false + +[[example]] +name = "core_widgets" +path = "examples/ui/core_widgets.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets] +name = "Core Widgets" +description = "Demonstrates use of core (headless) widgets in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "core_widgets_observers" +path = "examples/ui/core_widgets_observers.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets_observers] +name = "Core Widgets (w/Observers)" +description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers" +category = "UI (User Interface)" +wasm = true diff --git a/assets/branding/bevy_solari.svg b/assets/branding/bevy_solari.svg new file mode 100644 index 0000000000..65b996493f --- /dev/null +++ b/assets/branding/bevy_solari.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/shaders/array_texture.wgsl b/assets/shaders/array_texture.wgsl index d49d492a06..293f331665 100644 --- a/assets/shaders/array_texture.wgsl +++ b/assets/shaders/array_texture.wgsl @@ -7,8 +7,8 @@ } #import bevy_core_pipeline::tonemapping::tone_mapping -@group(2) @binding(0) var my_array_texture: texture_2d_array; -@group(2) @binding(1) var my_array_texture_sampler: sampler; +@group(3) @binding(0) var my_array_texture: texture_2d_array; +@group(3) @binding(1) var my_array_texture_sampler: sampler; @fragment fn fragment( diff --git a/assets/shaders/automatic_instancing.wgsl b/assets/shaders/automatic_instancing.wgsl index 35276246b0..ca2195df0d 100644 --- a/assets/shaders/automatic_instancing.wgsl +++ b/assets/shaders/automatic_instancing.wgsl @@ -3,8 +3,8 @@ view_transformations::position_world_to_clip } -@group(2) @binding(0) var texture: texture_2d; -@group(2) @binding(1) var texture_sampler: sampler; +@group(3) @binding(0) var texture: texture_2d; +@group(3) @binding(1) var texture_sampler: sampler; struct Vertex { @builtin(instance_index) instance_index: u32, diff --git a/assets/shaders/bindless_material.wgsl b/assets/shaders/bindless_material.wgsl index 3de313b81a..f7fa795d94 100644 --- a/assets/shaders/bindless_material.wgsl +++ b/assets/shaders/bindless_material.wgsl @@ -15,12 +15,12 @@ struct MaterialBindings { } #ifdef BINDLESS -@group(2) @binding(0) var materials: array; -@group(2) @binding(10) var material_color: binding_array; +@group(3) @binding(0) var materials: array; +@group(3) @binding(10) var material_color: binding_array; #else // BINDLESS -@group(2) @binding(0) var material_color: Color; -@group(2) @binding(1) var material_color_texture: texture_2d; -@group(2) @binding(2) var material_color_sampler: sampler; +@group(3) @binding(0) var material_color: Color; +@group(3) @binding(1) var material_color_texture: texture_2d; +@group(3) @binding(2) var material_color_sampler: sampler; #endif // BINDLESS @fragment diff --git a/assets/shaders/cubemap_unlit.wgsl b/assets/shaders/cubemap_unlit.wgsl index 14e45a045b..81e153e408 100644 --- a/assets/shaders/cubemap_unlit.wgsl +++ b/assets/shaders/cubemap_unlit.wgsl @@ -1,12 +1,12 @@ #import bevy_pbr::forward_io::VertexOutput #ifdef CUBEMAP_ARRAY -@group(2) @binding(0) var base_color_texture: texture_cube_array; +@group(3) @binding(0) var base_color_texture: texture_cube_array; #else -@group(2) @binding(0) var base_color_texture: texture_cube; +@group(3) @binding(0) var base_color_texture: texture_cube; #endif -@group(2) @binding(1) var base_color_sampler: sampler; +@group(3) @binding(1) var base_color_sampler: sampler; @fragment fn fragment( diff --git a/assets/shaders/custom_material.frag b/assets/shaders/custom_material.frag index a6bc9af0d2..0617a08ae7 100644 --- a/assets/shaders/custom_material.frag +++ b/assets/shaders/custom_material.frag @@ -3,10 +3,10 @@ layout(location = 0) in vec2 v_Uv; layout(location = 0) out vec4 o_Target; -layout(set = 2, binding = 0) uniform vec4 CustomMaterial_color; +layout(set = 3, binding = 0) uniform vec4 CustomMaterial_color; -layout(set = 2, binding = 1) uniform texture2D CustomMaterial_texture; -layout(set = 2, binding = 2) uniform sampler CustomMaterial_sampler; +layout(set = 3, binding = 1) uniform texture2D CustomMaterial_texture; +layout(set = 3, binding = 2) uniform sampler CustomMaterial_sampler; // wgsl modules can be imported and used in glsl // FIXME - this doesn't work any more ... diff --git a/assets/shaders/custom_material.vert b/assets/shaders/custom_material.vert index 86ca3629e2..f9a2813a37 100644 --- a/assets/shaders/custom_material.vert +++ b/assets/shaders/custom_material.vert @@ -25,9 +25,9 @@ struct Mesh { }; #ifdef PER_OBJECT_BUFFER_BATCH_SIZE -layout(set = 1, binding = 0) uniform Mesh Meshes[#{PER_OBJECT_BUFFER_BATCH_SIZE}]; +layout(set = 2, binding = 0) uniform Mesh Meshes[#{PER_OBJECT_BUFFER_BATCH_SIZE}]; #else -layout(set = 1, binding = 0) readonly buffer _Meshes { +layout(set = 2, binding = 0) readonly buffer _Meshes { Mesh Meshes[]; }; #endif // PER_OBJECT_BUFFER_BATCH_SIZE diff --git a/assets/shaders/custom_material.wesl b/assets/shaders/custom_material.wesl index 5113e1cbe0..ca94668784 100644 --- a/assets/shaders/custom_material.wesl +++ b/assets/shaders/custom_material.wesl @@ -10,7 +10,7 @@ struct CustomMaterial { time: vec4, } -@group(2) @binding(0) var material: CustomMaterial; +@group(3) @binding(0) var material: CustomMaterial; @fragment fn fragment( diff --git a/assets/shaders/custom_material.wgsl b/assets/shaders/custom_material.wgsl index 1b65627d45..7548d2223c 100644 --- a/assets/shaders/custom_material.wgsl +++ b/assets/shaders/custom_material.wgsl @@ -2,9 +2,9 @@ // we can import items from shader modules in the assets folder with a quoted path #import "shaders/custom_material_import.wgsl"::COLOR_MULTIPLIER -@group(2) @binding(0) var material_color: vec4; -@group(2) @binding(1) var material_color_texture: texture_2d; -@group(2) @binding(2) var material_color_sampler: sampler; +@group(3) @binding(0) var material_color: vec4; +@group(3) @binding(1) var material_color_texture: texture_2d; +@group(3) @binding(2) var material_color_sampler: sampler; @fragment fn fragment( diff --git a/assets/shaders/custom_material_screenspace_texture.wgsl b/assets/shaders/custom_material_screenspace_texture.wgsl index 36da2a7f8c..abad3cc15a 100644 --- a/assets/shaders/custom_material_screenspace_texture.wgsl +++ b/assets/shaders/custom_material_screenspace_texture.wgsl @@ -4,8 +4,8 @@ utils::coords_to_viewport_uv, } -@group(2) @binding(0) var texture: texture_2d; -@group(2) @binding(1) var texture_sampler: sampler; +@group(3) @binding(0) var texture: texture_2d; +@group(3) @binding(1) var texture_sampler: sampler; @fragment fn fragment( diff --git a/assets/shaders/custom_vertex_attribute.wgsl b/assets/shaders/custom_vertex_attribute.wgsl index f8062ab77b..6b7b93e4c7 100644 --- a/assets/shaders/custom_vertex_attribute.wgsl +++ b/assets/shaders/custom_vertex_attribute.wgsl @@ -3,7 +3,7 @@ struct CustomMaterial { color: vec4, }; -@group(2) @binding(0) var material: CustomMaterial; +@group(3) @binding(0) var material: CustomMaterial; struct Vertex { @builtin(instance_index) instance_index: u32, diff --git a/assets/shaders/extended_material.wgsl b/assets/shaders/extended_material.wgsl index fc69f30bb5..7bad24a331 100644 --- a/assets/shaders/extended_material.wgsl +++ b/assets/shaders/extended_material.wgsl @@ -19,7 +19,7 @@ struct MyExtendedMaterial { quantize_steps: u32, } -@group(2) @binding(100) +@group(3) @binding(100) var my_extended_material: MyExtendedMaterial; @fragment diff --git a/assets/shaders/extended_material_bindless.wgsl b/assets/shaders/extended_material_bindless.wgsl index f8650b0da7..c9cb07e0c7 100644 --- a/assets/shaders/extended_material_bindless.wgsl +++ b/assets/shaders/extended_material_bindless.wgsl @@ -42,19 +42,19 @@ struct ExampleBindlessExtendedMaterial { // The indices of the bindless resources in the bindless resource arrays, for // the `ExampleBindlessExtension` fields. -@group(2) @binding(100) var example_extended_material_indices: +@group(3) @binding(100) var example_extended_material_indices: array; // An array that holds the `ExampleBindlessExtendedMaterial` plain old data, // indexed by `ExampleBindlessExtendedMaterialIndices.material`. -@group(2) @binding(101) var example_extended_material: +@group(3) @binding(101) var example_extended_material: array; #else // BINDLESS // In non-bindless mode, we simply use a uniform for the plain old data. -@group(2) @binding(50) var example_extended_material: ExampleBindlessExtendedMaterial; -@group(2) @binding(51) var modulate_texture: texture_2d; -@group(2) @binding(52) var modulate_sampler: sampler; +@group(3) @binding(50) var example_extended_material: ExampleBindlessExtendedMaterial; +@group(3) @binding(51) var modulate_texture: texture_2d; +@group(3) @binding(52) var modulate_sampler: sampler; #endif // BINDLESS diff --git a/assets/shaders/fallback_image_test.wgsl b/assets/shaders/fallback_image_test.wgsl index c48f091bcc..c92cbd1577 100644 --- a/assets/shaders/fallback_image_test.wgsl +++ b/assets/shaders/fallback_image_test.wgsl @@ -1,22 +1,22 @@ #import bevy_pbr::forward_io::VertexOutput -@group(2) @binding(0) var test_texture_1d: texture_1d; -@group(2) @binding(1) var test_texture_1d_sampler: sampler; +@group(3) @binding(0) var test_texture_1d: texture_1d; +@group(3) @binding(1) var test_texture_1d_sampler: sampler; -@group(2) @binding(2) var test_texture_2d: texture_2d; -@group(2) @binding(3) var test_texture_2d_sampler: sampler; +@group(3) @binding(2) var test_texture_2d: texture_2d; +@group(3) @binding(3) var test_texture_2d_sampler: sampler; -@group(2) @binding(4) var test_texture_2d_array: texture_2d_array; -@group(2) @binding(5) var test_texture_2d_array_sampler: sampler; +@group(3) @binding(4) var test_texture_2d_array: texture_2d_array; +@group(3) @binding(5) var test_texture_2d_array_sampler: sampler; -@group(2) @binding(6) var test_texture_cube: texture_cube; -@group(2) @binding(7) var test_texture_cube_sampler: sampler; +@group(3) @binding(6) var test_texture_cube: texture_cube; +@group(3) @binding(7) var test_texture_cube_sampler: sampler; -@group(2) @binding(8) var test_texture_cube_array: texture_cube_array; -@group(2) @binding(9) var test_texture_cube_array_sampler: sampler; +@group(3) @binding(8) var test_texture_cube_array: texture_cube_array; +@group(3) @binding(9) var test_texture_cube_array_sampler: sampler; -@group(2) @binding(10) var test_texture_3d: texture_3d; -@group(2) @binding(11) var test_texture_3d_sampler: sampler; +@group(3) @binding(10) var test_texture_3d: texture_3d; +@group(3) @binding(11) var test_texture_3d_sampler: sampler; @fragment fn fragment(in: VertexOutput) {} diff --git a/assets/shaders/irradiance_volume_voxel_visualization.wgsl b/assets/shaders/irradiance_volume_voxel_visualization.wgsl index f34e6f8453..0e12110f3b 100644 --- a/assets/shaders/irradiance_volume_voxel_visualization.wgsl +++ b/assets/shaders/irradiance_volume_voxel_visualization.wgsl @@ -12,7 +12,7 @@ struct VoxelVisualizationIrradianceVolumeInfo { intensity: f32, } -@group(2) @binding(100) +@group(3) @binding(100) var irradiance_volume_info: VoxelVisualizationIrradianceVolumeInfo; @fragment diff --git a/assets/shaders/line_material.wgsl b/assets/shaders/line_material.wgsl index cc7ca047d5..639762a444 100644 --- a/assets/shaders/line_material.wgsl +++ b/assets/shaders/line_material.wgsl @@ -4,7 +4,7 @@ struct LineMaterial { color: vec4, }; -@group(2) @binding(0) var material: LineMaterial; +@group(3) @binding(0) var material: LineMaterial; @fragment fn fragment( diff --git a/assets/shaders/shader_defs.wgsl b/assets/shaders/shader_defs.wgsl index fdddc4caa1..776fc9ffe6 100644 --- a/assets/shaders/shader_defs.wgsl +++ b/assets/shaders/shader_defs.wgsl @@ -4,7 +4,7 @@ struct CustomMaterial { color: vec4, }; -@group(2) @binding(0) var material: CustomMaterial; +@group(3) @binding(0) var material: CustomMaterial; @fragment fn fragment( diff --git a/assets/shaders/show_prepass.wgsl b/assets/shaders/show_prepass.wgsl index c1b3a89750..b1a53677ac 100644 --- a/assets/shaders/show_prepass.wgsl +++ b/assets/shaders/show_prepass.wgsl @@ -11,7 +11,7 @@ struct ShowPrepassSettings { padding_1: u32, padding_2: u32, } -@group(2) @binding(0) var settings: ShowPrepassSettings; +@group(3) @binding(0) var settings: ShowPrepassSettings; @fragment fn fragment( diff --git a/assets/shaders/storage_buffer.wgsl b/assets/shaders/storage_buffer.wgsl index c27053b9a2..f447333cc9 100644 --- a/assets/shaders/storage_buffer.wgsl +++ b/assets/shaders/storage_buffer.wgsl @@ -3,7 +3,7 @@ view_transformations::position_world_to_clip } -@group(2) @binding(0) var colors: array, 5>; +@group(3) @binding(0) var colors: array, 5>; struct Vertex { @builtin(instance_index) instance_index: u32, diff --git a/assets/shaders/texture_binding_array.wgsl b/assets/shaders/texture_binding_array.wgsl index de7a4e1b96..17b94a74d3 100644 --- a/assets/shaders/texture_binding_array.wgsl +++ b/assets/shaders/texture_binding_array.wgsl @@ -1,7 +1,7 @@ #import bevy_pbr::forward_io::VertexOutput -@group(2) @binding(0) var textures: binding_array>; -@group(2) @binding(1) var nearest_sampler: sampler; +@group(3) @binding(0) var textures: binding_array>; +@group(3) @binding(1) var nearest_sampler: sampler; // We can also have array of samplers // var samplers: binding_array; diff --git a/assets/shaders/water_material.wgsl b/assets/shaders/water_material.wgsl index 31d04b5f11..a8a9e03df4 100644 --- a/assets/shaders/water_material.wgsl +++ b/assets/shaders/water_material.wgsl @@ -23,9 +23,9 @@ struct WaterSettings { @group(0) @binding(1) var globals: Globals; -@group(2) @binding(100) var water_normals_texture: texture_2d; -@group(2) @binding(101) var water_normals_sampler: sampler; -@group(2) @binding(102) var water_settings: WaterSettings; +@group(3) @binding(100) var water_normals_texture: texture_2d; +@group(3) @binding(101) var water_normals_sampler: sampler; +@group(3) @binding(102) var water_settings: WaterSettings; // Samples a single octave of noise and returns the resulting normal. fn sample_noise_octave(uv: vec2, strength: f32) -> vec3 { diff --git a/benches/benches/bevy_ecs/change_detection.rs b/benches/benches/bevy_ecs/change_detection.rs index 92f3251abc..3cfa5bcbed 100644 --- a/benches/benches/bevy_ecs/change_detection.rs +++ b/benches/benches/bevy_ecs/change_detection.rs @@ -49,6 +49,7 @@ impl BenchModify for Table { black_box(self.0) } } + impl BenchModify for Sparse { fn bench_modify(&mut self) -> f32 { self.0 += 1f32; diff --git a/benches/benches/bevy_ecs/entity_cloning.rs b/benches/benches/bevy_ecs/entity_cloning.rs index 0eaae27ce4..44ffa1d52b 100644 --- a/benches/benches/bevy_ecs/entity_cloning.rs +++ b/benches/benches/bevy_ecs/entity_cloning.rs @@ -1,7 +1,7 @@ use core::hint::black_box; use benches::bench; -use bevy_ecs::bundle::Bundle; +use bevy_ecs::bundle::{Bundle, InsertMode}; use bevy_ecs::component::ComponentCloneBehavior; use bevy_ecs::entity::EntityCloner; use bevy_ecs::hierarchy::ChildOf; @@ -17,41 +17,15 @@ criterion_group!( hierarchy_tall, hierarchy_wide, hierarchy_many, + filter ); #[derive(Component, Reflect, Default, Clone)] -struct C1(Mat4); +struct C(Mat4); -#[derive(Component, Reflect, Default, Clone)] -struct C2(Mat4); +type ComplexBundle = (C<1>, C<2>, C<3>, C<4>, C<5>, C<6>, C<7>, C<8>, C<9>, C<10>); -#[derive(Component, Reflect, Default, Clone)] -struct C3(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C4(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C5(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C6(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C7(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C8(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C9(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C10(Mat4); - -type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10); - -/// Sets the [`ComponentCloneHandler`] for all explicit and required components in a bundle `B` to +/// Sets the [`ComponentCloneBehavior`] for all explicit and required components in a bundle `B` to /// use the [`Reflect`] trait instead of [`Clone`]. fn reflection_cloner( world: &mut World, @@ -71,7 +45,7 @@ fn reflection_cloner( // this bundle are saved. let component_ids: Vec<_> = world.register_bundle::().contributed_components().into(); - let mut builder = EntityCloner::build(world); + let mut builder = EntityCloner::build_opt_out(world); // Overwrite the clone handler for all components in the bundle to use `Reflect`, not `Clone`. for component in component_ids { @@ -82,16 +56,15 @@ fn reflection_cloner( builder.finish() } -/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a -/// bundle `B`. +/// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`. /// /// The bundle must implement [`Default`], which is used to create the first entity that gets cloned /// in the benchmark. /// -/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneHandler`] for all -/// components (which is usually [`ComponentCloneHandler::clone_handler()`]). If `clone_via_reflect` +/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneBehavior`] for all +/// components (which is usually [`ComponentCloneBehavior::clone()`]). If `clone_via_reflect` /// is true, it will overwrite the handler for all components in the bundle to be -/// [`ComponentCloneHandler::reflect_handler()`]. +/// [`ComponentCloneBehavior::reflect()`]. fn bench_clone( b: &mut Bencher, clone_via_reflect: bool, @@ -114,8 +87,7 @@ fn bench_clone( }); } -/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a -/// bundle `B`. +/// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`. /// /// As compared to [`bench_clone()`], this benchmarks recursively cloning an entity with several /// children. It does so by setting up an entity tree with a given `height` where each entity has a @@ -135,7 +107,7 @@ fn bench_clone_hierarchy( let mut cloner = if clone_via_reflect { reflection_cloner::(&mut world, true) } else { - let mut builder = EntityCloner::build(&mut world); + let mut builder = EntityCloner::build_opt_out(&mut world); builder.linked_cloning(true); builder.finish() }; @@ -169,7 +141,7 @@ fn bench_clone_hierarchy( // Each benchmark runs twice: using either the `Clone` or `Reflect` traits to clone entities. This // constant represents this as an easy array that can be used in a `for` loop. -const SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)]; +const CLONE_SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)]; /// Benchmarks cloning a single entity with 10 components and no children. fn single(c: &mut Criterion) { @@ -178,7 +150,7 @@ fn single(c: &mut Criterion) { // We're cloning 1 entity. group.throughput(Throughput::Elements(1)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { bench_clone::(b, clone_via_reflect); }); @@ -194,9 +166,9 @@ fn hierarchy_tall(c: &mut Criterion) { // We're cloning both the root entity and its 50 descendents. group.throughput(Throughput::Elements(51)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { - bench_clone_hierarchy::(b, 50, 1, clone_via_reflect); + bench_clone_hierarchy::>(b, 50, 1, clone_via_reflect); }); } @@ -210,9 +182,9 @@ fn hierarchy_wide(c: &mut Criterion) { // We're cloning both the root entity and its 50 direct children. group.throughput(Throughput::Elements(51)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { - bench_clone_hierarchy::(b, 1, 50, clone_via_reflect); + bench_clone_hierarchy::>(b, 1, 50, clone_via_reflect); }); } @@ -228,7 +200,7 @@ fn hierarchy_many(c: &mut Criterion) { // of entities spawned in `bench_clone_hierarchy()` with a `println!()` statement. :) group.throughput(Throughput::Elements(364)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { bench_clone_hierarchy::(b, 5, 3, clone_via_reflect); }); @@ -236,3 +208,157 @@ fn hierarchy_many(c: &mut Criterion) { group.finish(); } + +/// Filter scenario variant for bot opt-in and opt-out filters +#[derive(Clone, Copy)] +#[expect( + clippy::enum_variant_names, + reason = "'Opt' is not understood as an prefix but `OptOut'/'OptIn' are" +)] +enum FilterScenario { + OptOutNone, + OptOutNoneKeep(bool), + OptOutAll, + OptInNone, + OptInAll, + OptInAllWithoutRequired, + OptInAllKeep(bool), + OptInAllKeepWithoutRequired(bool), +} + +impl From for String { + fn from(value: FilterScenario) -> Self { + match value { + FilterScenario::OptOutNone => "opt_out_none", + FilterScenario::OptOutNoneKeep(true) => "opt_out_none_keep_none", + FilterScenario::OptOutNoneKeep(false) => "opt_out_none_keep_all", + FilterScenario::OptOutAll => "opt_out_all", + FilterScenario::OptInNone => "opt_in_none", + FilterScenario::OptInAll => "opt_in_all", + FilterScenario::OptInAllWithoutRequired => "opt_in_all_without_required", + FilterScenario::OptInAllKeep(true) => "opt_in_all_keep_none", + FilterScenario::OptInAllKeep(false) => "opt_in_all_keep_all", + FilterScenario::OptInAllKeepWithoutRequired(true) => { + "opt_in_all_keep_none_without_required" + } + FilterScenario::OptInAllKeepWithoutRequired(false) => { + "opt_in_all_keep_all_without_required" + } + } + .into() + } +} + +/// Common scenarios for different filter to be benchmarked. +const FILTER_SCENARIOS: [FilterScenario; 11] = [ + FilterScenario::OptOutNone, + FilterScenario::OptOutNoneKeep(true), + FilterScenario::OptOutNoneKeep(false), + FilterScenario::OptOutAll, + FilterScenario::OptInNone, + FilterScenario::OptInAll, + FilterScenario::OptInAllWithoutRequired, + FilterScenario::OptInAllKeep(true), + FilterScenario::OptInAllKeep(false), + FilterScenario::OptInAllKeepWithoutRequired(true), + FilterScenario::OptInAllKeepWithoutRequired(false), +]; + +/// A helper function that benchmarks running [`EntityCloner::clone_entity`] with a bundle `B`. +/// +/// The bundle must implement [`Default`], which is used to create the first entity that gets its components cloned +/// in the benchmark. It may also be used to populate the target entity depending on the scenario. +fn bench_filter(b: &mut Bencher, scenario: FilterScenario) { + let mut world = World::default(); + let mut spawn = |empty| match empty { + false => world.spawn(B::default()).id(), + true => world.spawn_empty().id(), + }; + let source = spawn(false); + let (target, mut cloner); + + match scenario { + FilterScenario::OptOutNone => { + target = spawn(true); + cloner = EntityCloner::default(); + } + FilterScenario::OptOutNoneKeep(is_new) => { + target = spawn(is_new); + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.insert_mode(InsertMode::Keep); + cloner = builder.finish(); + } + FilterScenario::OptOutAll => { + target = spawn(true); + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.deny::(); + cloner = builder.finish(); + } + FilterScenario::OptInNone => { + target = spawn(true); + let builder = EntityCloner::build_opt_in(&mut world); + cloner = builder.finish(); + } + FilterScenario::OptInAll => { + target = spawn(true); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.allow::(); + cloner = builder.finish(); + } + FilterScenario::OptInAllWithoutRequired => { + target = spawn(true); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.without_required_components(|builder| { + builder.allow::(); + }); + cloner = builder.finish(); + } + FilterScenario::OptInAllKeep(is_new) => { + target = spawn(is_new); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.allow_if_new::(); + cloner = builder.finish(); + } + FilterScenario::OptInAllKeepWithoutRequired(is_new) => { + target = spawn(is_new); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.without_required_components(|builder| { + builder.allow_if_new::(); + }); + cloner = builder.finish(); + } + } + + b.iter(|| { + // clones the given entity into the target + cloner.clone_entity(&mut world, black_box(source), black_box(target)); + world.flush(); + }); +} + +/// Benchmarks filtering of cloning a single entity with 5 unclonable components (each requiring 1 unclonable component) into a target. +fn filter(c: &mut Criterion) { + #[derive(Component, Default)] + #[component(clone_behavior = Ignore)] + struct C; + + #[derive(Component, Default)] + #[component(clone_behavior = Ignore)] + #[require(C::)] + struct R; + + type RequiringBundle = (R<1>, R<2>, R<3>, R<4>, R<5>); + + let mut group = c.benchmark_group(bench!("filter")); + + // We're cloning 1 entity into a target. + group.throughput(Throughput::Elements(1)); + + for scenario in FILTER_SCENARIOS { + group.bench_function(scenario, |b| { + bench_filter::(b, scenario); + }); + } + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/events/iter.rs b/benches/benches/bevy_ecs/events/iter.rs index dc20bc3395..9ad17ed8c8 100644 --- a/benches/benches/bevy_ecs/events/iter.rs +++ b/benches/benches/bevy_ecs/events/iter.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct BenchEvent([u8; SIZE]); pub struct Benchmark(Events>); diff --git a/benches/benches/bevy_ecs/events/mod.rs b/benches/benches/bevy_ecs/events/mod.rs index b87a138e06..c2c7f3ee28 100644 --- a/benches/benches/bevy_ecs/events/mod.rs +++ b/benches/benches/bevy_ecs/events/mod.rs @@ -10,19 +10,19 @@ fn send(c: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); for count in [100, 1_000, 10_000] { - group.bench_function(format!("size_4_events_{}", count), |b| { + group.bench_function(format!("size_4_events_{count}"), |b| { let mut bench = send::Benchmark::<4>::new(count); b.iter(move || bench.run()); }); } for count in [100, 1_000, 10_000] { - group.bench_function(format!("size_16_events_{}", count), |b| { + group.bench_function(format!("size_16_events_{count}"), |b| { let mut bench = send::Benchmark::<16>::new(count); b.iter(move || bench.run()); }); } for count in [100, 1_000, 10_000] { - group.bench_function(format!("size_512_events_{}", count), |b| { + group.bench_function(format!("size_512_events_{count}"), |b| { let mut bench = send::Benchmark::<512>::new(count); b.iter(move || bench.run()); }); @@ -35,19 +35,19 @@ fn iter(c: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); for count in [100, 1_000, 10_000] { - group.bench_function(format!("size_4_events_{}", count), |b| { + group.bench_function(format!("size_4_events_{count}"), |b| { let mut bench = iter::Benchmark::<4>::new(count); b.iter(move || bench.run()); }); } for count in [100, 1_000, 10_000] { - group.bench_function(format!("size_16_events_{}", count), |b| { + group.bench_function(format!("size_16_events_{count}"), |b| { let mut bench = iter::Benchmark::<4>::new(count); b.iter(move || bench.run()); }); } for count in [100, 1_000, 10_000] { - group.bench_function(format!("size_512_events_{}", count), |b| { + group.bench_function(format!("size_512_events_{count}"), |b| { let mut bench = iter::Benchmark::<512>::new(count); b.iter(move || bench.run()); }); diff --git a/benches/benches/bevy_ecs/events/send.rs b/benches/benches/bevy_ecs/events/send.rs index fa996b50aa..be8934e789 100644 --- a/benches/benches/bevy_ecs/events/send.rs +++ b/benches/benches/bevy_ecs/events/send.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct BenchEvent([u8; SIZE]); impl Default for BenchEvent { diff --git a/benches/benches/bevy_ecs/iteration/mod.rs b/benches/benches/bevy_ecs/iteration/mod.rs index 0fa7aced28..b296c5ce0b 100644 --- a/benches/benches/bevy_ecs/iteration/mod.rs +++ b/benches/benches/bevy_ecs/iteration/mod.rs @@ -130,7 +130,7 @@ fn par_iter_simple(c: &mut Criterion) { group.warm_up_time(core::time::Duration::from_millis(500)); group.measurement_time(core::time::Duration::from_secs(4)); for f in [0, 10, 100, 1000] { - group.bench_function(format!("with_{}_fragment", f), |b| { + group.bench_function(format!("with_{f}_fragment"), |b| { let mut bench = par_iter_simple::Benchmark::new(f); b.iter(move || bench.run()); }); diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 65c15f7308..808c3727d5 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -61,14 +61,10 @@ pub fn event_propagation(criterion: &mut Criterion) { group.finish(); } -#[derive(Clone, Component)] +#[derive(Event, EntityEvent, Clone, Component)] +#[entity_event(traversal = &'static ChildOf, auto_propagate)] struct TestEvent {} -impl Event for TestEvent { - type Traversal = &'static ChildOf; - const AUTO_PROPAGATE: bool = true; -} - fn send_events(world: &mut World, leaves: &[Entity]) { let target = leaves.iter().choose(&mut rand::thread_rng()).unwrap(); @@ -117,6 +113,6 @@ fn add_listeners_to_hierarchy( } } -fn empty_listener(trigger: Trigger>) { +fn empty_listener(trigger: On>) { black_box(trigger); } diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 85207624e8..9c26b074e5 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,8 +1,8 @@ use core::hint::black_box; use bevy_ecs::{ - event::Event, - observer::{Trigger, TriggerTargets}, + event::{EntityEvent, Event}, + observer::{On, TriggerTargets}, world::World, }; @@ -13,7 +13,7 @@ fn deterministic_rand() -> ChaCha8Rng { ChaCha8Rng::seed_from_u64(42) } -#[derive(Clone, Event)] +#[derive(Clone, Event, EntityEvent)] struct EventBase; pub fn observe_simple(criterion: &mut Criterion) { @@ -46,7 +46,7 @@ pub fn observe_simple(criterion: &mut Criterion) { group.finish(); } -fn empty_listener_base(trigger: Trigger) { +fn empty_listener_base(trigger: On) { black_box(trigger); } diff --git a/benches/benches/bevy_ecs/scheduling/run_condition.rs b/benches/benches/bevy_ecs/scheduling/run_condition.rs index 7b9cf418f4..9c40cf396e 100644 --- a/benches/benches/bevy_ecs/scheduling/run_condition.rs +++ b/benches/benches/bevy_ecs/scheduling/run_condition.rs @@ -24,7 +24,7 @@ pub fn run_condition_yes(criterion: &mut Criterion) { } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{}_systems", amount), |bencher| { + group.bench_function(format!("{amount}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -46,7 +46,7 @@ pub fn run_condition_no(criterion: &mut Criterion) { } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{}_systems", amount), |bencher| { + group.bench_function(format!("{amount}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -77,7 +77,7 @@ pub fn run_condition_yes_with_query(criterion: &mut Criterion) { } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{}_systems", amount), |bencher| { + group.bench_function(format!("{amount}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -105,7 +105,7 @@ pub fn run_condition_yes_with_resource(criterion: &mut Criterion) { } // run once to initialize systems schedule.run(&mut world); - group.bench_function(format!("{}_systems", amount), |bencher| { + group.bench_function(format!("{amount}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); diff --git a/benches/benches/bevy_ecs/scheduling/running_systems.rs b/benches/benches/bevy_ecs/scheduling/running_systems.rs index 2fc1da1710..159974117c 100644 --- a/benches/benches/bevy_ecs/scheduling/running_systems.rs +++ b/benches/benches/bevy_ecs/scheduling/running_systems.rs @@ -26,7 +26,7 @@ pub fn empty_systems(criterion: &mut Criterion) { schedule.add_systems(empty); } schedule.run(&mut world); - group.bench_function(format!("{}_systems", amount), |bencher| { + group.bench_function(format!("{amount}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -38,7 +38,7 @@ pub fn empty_systems(criterion: &mut Criterion) { schedule.add_systems((empty, empty, empty, empty, empty)); } schedule.run(&mut world); - group.bench_function(format!("{}_systems", amount), |bencher| { + group.bench_function(format!("{amount}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); }); @@ -79,10 +79,7 @@ pub fn busy_systems(criterion: &mut Criterion) { } schedule.run(&mut world); group.bench_function( - format!( - "{:02}x_entities_{:02}_systems", - entity_bunches, system_amount - ), + format!("{entity_bunches:02}x_entities_{system_amount:02}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); @@ -128,10 +125,7 @@ pub fn contrived(criterion: &mut Criterion) { } schedule.run(&mut world); group.bench_function( - format!( - "{:02}x_entities_{:02}_systems", - entity_bunches, system_amount - ), + format!("{entity_bunches:02}x_entities_{system_amount:02}_systems"), |bencher| { bencher.iter(|| { schedule.run(&mut world); diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index bedfb8e5af..4836a243eb 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -37,7 +37,7 @@ pub fn spawn_commands(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in [100, 1_000, 10_000] { - group.bench_function(format!("{}_entities", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities"), |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); @@ -68,7 +68,7 @@ pub fn nonempty_spawn_commands(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in [100, 1_000, 10_000] { - group.bench_function(format!("{}_entities", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities"), |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); @@ -162,7 +162,7 @@ pub fn fake_commands(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for command_count in [100, 1_000, 10_000] { - group.bench_function(format!("{}_commands", command_count), |bencher| { + group.bench_function(format!("{command_count}_commands"), |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); @@ -207,7 +207,7 @@ pub fn sized_commands_impl(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for command_count in [100, 1_000, 10_000] { - group.bench_function(format!("{}_commands", command_count), |bencher| { + group.bench_function(format!("{command_count}_commands"), |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); diff --git a/benches/benches/bevy_ecs/world/despawn.rs b/benches/benches/bevy_ecs/world/despawn.rs index cd693fc15c..7b79ed95d9 100644 --- a/benches/benches/bevy_ecs/world/despawn.rs +++ b/benches/benches/bevy_ecs/world/despawn.rs @@ -13,7 +13,7 @@ pub fn world_despawn(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in [1, 100, 10_000] { - group.bench_function(format!("{}_entities", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities"), |bencher| { bencher.iter_batched_ref( || { let mut world = World::default(); diff --git a/benches/benches/bevy_ecs/world/despawn_recursive.rs b/benches/benches/bevy_ecs/world/despawn_recursive.rs index 78c644174b..f63c1a510b 100644 --- a/benches/benches/bevy_ecs/world/despawn_recursive.rs +++ b/benches/benches/bevy_ecs/world/despawn_recursive.rs @@ -13,7 +13,7 @@ pub fn world_despawn_recursive(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in [1, 100, 10_000] { - group.bench_function(format!("{}_entities", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities"), |bencher| { bencher.iter_batched_ref( || { let mut world = World::default(); diff --git a/benches/benches/bevy_ecs/world/spawn.rs b/benches/benches/bevy_ecs/world/spawn.rs index 502d10ceb3..9cceda7ae5 100644 --- a/benches/benches/bevy_ecs/world/spawn.rs +++ b/benches/benches/bevy_ecs/world/spawn.rs @@ -13,7 +13,7 @@ pub fn world_spawn(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in [1, 100, 10_000] { - group.bench_function(format!("{}_entities", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities"), |bencher| { let mut world = World::default(); bencher.iter(|| { for _ in 0..entity_count { diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index e6e2a0bb90..81e0bf2b0f 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -49,7 +49,7 @@ pub fn world_entity(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in RANGE.map(|i| i * 10_000) { - group.bench_function(format!("{}_entities", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities"), |bencher| { let world = setup::(entity_count); bencher.iter(|| { @@ -72,7 +72,7 @@ pub fn world_get(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in RANGE.map(|i| i * 10_000) { - group.bench_function(format!("{}_entities_table", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let world = setup::
(entity_count); bencher.iter(|| { @@ -84,7 +84,7 @@ pub fn world_get(criterion: &mut Criterion) { } }); }); - group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let world = setup::(entity_count); bencher.iter(|| { @@ -107,7 +107,7 @@ pub fn world_query_get(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in RANGE.map(|i| i * 10_000) { - group.bench_function(format!("{}_entities_table", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let mut world = setup::
(entity_count); let mut query = world.query::<&Table>(); @@ -120,7 +120,7 @@ pub fn world_query_get(criterion: &mut Criterion) { } }); }); - group.bench_function(format!("{}_entities_table_wide", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_table_wide"), |bencher| { let mut world = setup_wide::<( WideTable<0>, WideTable<1>, @@ -147,7 +147,7 @@ pub fn world_query_get(criterion: &mut Criterion) { } }); }); - group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let mut world = setup::(entity_count); let mut query = world.query::<&Sparse>(); @@ -160,37 +160,33 @@ pub fn world_query_get(criterion: &mut Criterion) { } }); }); - group.bench_function( - format!("{}_entities_sparse_wide", entity_count), - |bencher| { - let mut world = setup_wide::<( - WideSparse<0>, - WideSparse<1>, - WideSparse<2>, - WideSparse<3>, - WideSparse<4>, - WideSparse<5>, - )>(entity_count); - let mut query = world.query::<( - &WideSparse<0>, - &WideSparse<1>, - &WideSparse<2>, - &WideSparse<3>, - &WideSparse<4>, - &WideSparse<5>, - )>(); + group.bench_function(format!("{entity_count}_entities_sparse_wide"), |bencher| { + let mut world = setup_wide::<( + WideSparse<0>, + WideSparse<1>, + WideSparse<2>, + WideSparse<3>, + WideSparse<4>, + WideSparse<5>, + )>(entity_count); + let mut query = world.query::<( + &WideSparse<0>, + &WideSparse<1>, + &WideSparse<2>, + &WideSparse<3>, + &WideSparse<4>, + &WideSparse<5>, + )>(); - bencher.iter(|| { - for i in 0..entity_count { - // SAFETY: Range is exclusive. - let entity = Entity::from_raw(EntityRow::new(unsafe { - NonMaxU32::new_unchecked(i) - })); - assert!(query.get(&world, entity).is_ok()); - } - }); - }, - ); + bencher.iter(|| { + for i in 0..entity_count { + // SAFETY: Range is exclusive. + let entity = + Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); + assert!(query.get(&world, entity).is_ok()); + } + }); + }); } group.finish(); @@ -202,7 +198,7 @@ pub fn world_query_iter(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in RANGE.map(|i| i * 10_000) { - group.bench_function(format!("{}_entities_table", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let mut world = setup::
(entity_count); let mut query = world.query::<&Table>(); @@ -216,7 +212,7 @@ pub fn world_query_iter(criterion: &mut Criterion) { assert_eq!(black_box(count), entity_count); }); }); - group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let mut world = setup::(entity_count); let mut query = world.query::<&Sparse>(); @@ -241,7 +237,7 @@ pub fn world_query_for_each(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in RANGE.map(|i| i * 10_000) { - group.bench_function(format!("{}_entities_table", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let mut world = setup::
(entity_count); let mut query = world.query::<&Table>(); @@ -255,7 +251,7 @@ pub fn world_query_for_each(criterion: &mut Criterion) { assert_eq!(black_box(count), entity_count); }); }); - group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let mut world = setup::(entity_count); let mut query = world.query::<&Sparse>(); @@ -280,7 +276,7 @@ pub fn query_get(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(4)); for entity_count in RANGE.map(|i| i * 10_000) { - group.bench_function(format!("{}_entities_table", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let mut world = World::default(); let mut entities: Vec<_> = world .spawn_batch((0..entity_count).map(|_| Table::default())) @@ -299,7 +295,7 @@ pub fn query_get(criterion: &mut Criterion) { assert_eq!(black_box(count), entity_count); }); }); - group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let mut world = World::default(); let mut entities: Vec<_> = world .spawn_batch((0..entity_count).map(|_| Sparse::default())) @@ -329,7 +325,7 @@ pub fn query_get_many(criterion: &mut Criterion) { group.measurement_time(core::time::Duration::from_secs(2 * N as u64)); for entity_count in RANGE.map(|i| i * 10_000) { - group.bench_function(format!("{}_calls_table", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_calls_table"), |bencher| { let mut world = World::default(); let mut entity_groups: Vec<_> = (0..entity_count) .map(|_| [(); N].map(|_| world.spawn(Table::default()).id())) @@ -352,7 +348,7 @@ pub fn query_get_many(criterion: &mut Criterion) { assert_eq!(black_box(count), entity_count); }); }); - group.bench_function(format!("{}_calls_sparse", entity_count), |bencher| { + group.bench_function(format!("{entity_count}_calls_sparse"), |bencher| { let mut world = World::default(); let mut entity_groups: Vec<_> = (0..entity_count) .map(|_| [(); N].map(|_| world.spawn(Sparse::default()).id())) diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs index 871a6d1062..e9fd0caf9f 100644 --- a/benches/benches/bevy_picking/ray_mesh_intersection.rs +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -155,6 +155,7 @@ fn bench(c: &mut Criterion) { &mesh.positions, Some(&mesh.normals), Some(&mesh.indices), + None, backface_culling, ); diff --git a/benches/benches/bevy_reflect/map.rs b/benches/benches/bevy_reflect/map.rs index 1eab01a587..a70f89e12b 100644 --- a/benches/benches/bevy_reflect/map.rs +++ b/benches/benches/bevy_reflect/map.rs @@ -142,7 +142,7 @@ fn concrete_map_apply(criterion: &mut Criterion) { fn u64_to_n_byte_key(k: u64, n: usize) -> String { let mut key = String::with_capacity(n); - write!(&mut key, "{}", k).unwrap(); + write!(&mut key, "{k}").unwrap(); // Pad key to n bytes. key.extend(iter::repeat_n('\0', n - key.len())); diff --git a/benches/benches/bevy_reflect/struct.rs b/benches/benches/bevy_reflect/struct.rs index 7750213b6d..52d539f64d 100644 --- a/benches/benches/bevy_reflect/struct.rs +++ b/benches/benches/bevy_reflect/struct.rs @@ -55,7 +55,7 @@ fn concrete_struct_field(criterion: &mut Criterion) { &s, |bencher, s| { let field_names = (0..field_count) - .map(|i| format!("field_{}", i)) + .map(|i| format!("field_{i}")) .collect::>(); bencher.iter(|| { @@ -256,7 +256,7 @@ fn dynamic_struct_apply(criterion: &mut Criterion) { let mut base = DynamicStruct::default(); for i in 0..field_count { - let field_name = format!("field_{}", i); + let field_name = format!("field_{i}"); base.insert(&field_name, 1u32); } @@ -283,7 +283,7 @@ fn dynamic_struct_apply(criterion: &mut Criterion) { let mut base = DynamicStruct::default(); let mut patch = DynamicStruct::default(); for i in 0..field_count { - let field_name = format!("field_{}", i); + let field_name = format!("field_{i}"); base.insert(&field_name, 0u32); patch.insert(&field_name, 1u32); } @@ -309,11 +309,11 @@ fn dynamic_struct_insert(criterion: &mut Criterion) { |bencher, field_count| { let mut s = DynamicStruct::default(); for i in 0..*field_count { - let field_name = format!("field_{}", i); + let field_name = format!("field_{i}"); s.insert(&field_name, ()); } - let field = format!("field_{}", field_count); + let field = format!("field_{field_count}"); bencher.iter_batched( || s.to_dynamic_struct(), |mut s| { @@ -339,7 +339,7 @@ fn dynamic_struct_get_field(criterion: &mut Criterion) { |bencher, field_count| { let mut s = DynamicStruct::default(); for i in 0..*field_count { - let field_name = format!("field_{}", i); + let field_name = format!("field_{i}"); s.insert(&field_name, ()); } diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 39628ec046..70ee16cf77 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_a11y" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides accessibility support for Bevy Engine" homepage = "https://bevy.org" @@ -40,10 +40,10 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } # other accesskit = { version = "0.19", default-features = false } diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index f8c46757dd..22b2f71f07 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -26,7 +26,8 @@ use accesskit::Node; use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - prelude::{Component, Event}, + component::Component, + event::{BufferedEvent, Event}, resource::Resource, schedule::SystemSet, }; @@ -44,7 +45,7 @@ use serde::{Deserialize, Serialize}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`. -#[derive(Event, Deref, DerefMut)] +#[derive(Event, BufferedEvent, Deref, DerefMut)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct ActionRequest(pub accesskit::ActionRequest); diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 9f9cd26587..637231fd4c 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_animation" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides animation functionality for Bevy Engine" homepage = "https://bevy.org" @@ -10,34 +10,34 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "petgraph", ] } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } # other petgraph = { version = "0.7", features = ["serde-1"] } -ron = "0.8" +ron = "0.10" serde = "1" blake3 = { version = "1.0" } downcast-rs = { version = "2", default-features = false, features = ["std"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } either = "1.13" thread_local = "1" uuid = { version = "1.13.1", features = ["v4"] } diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aa6d252fee..a5f4041ac7 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -1,10 +1,11 @@ //! The animation graph, which allows animations to be blended together. use core::{ + fmt::Write, iter, ops::{Index, IndexMut, Range}, }; -use std::io::{self, Write}; +use std::io; use bevy_asset::{ io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext, diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index dd68595961..ae7ce42ed6 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -324,13 +324,13 @@ impl AnimationClip { .push(variable_curve); } - /// Add a untargeted [`Event`] to this [`AnimationClip`]. + /// Add an [`EntityEvent`] with no [`AnimationTarget`] to this [`AnimationClip`]. /// /// The `event` will be cloned and triggered on the [`AnimationPlayer`] entity once the `time` (in seconds) /// is reached in the animation. /// /// See also [`add_event_to_target`](Self::add_event_to_target). - pub fn add_event(&mut self, time: f32, event: impl Event + Clone) { + pub fn add_event(&mut self, time: f32, event: impl EntityEvent + Clone) { self.add_event_fn( time, move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { @@ -339,7 +339,7 @@ impl AnimationClip { ); } - /// Add an [`Event`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// Add an [`EntityEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// /// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds) /// is reached in the animation. @@ -349,7 +349,7 @@ impl AnimationClip { &mut self, target_id: AnimationTargetId, time: f32, - event: impl Event + Clone, + event: impl EntityEvent + Clone, ) { self.add_event_fn_to_target( target_id, @@ -360,19 +360,19 @@ impl AnimationClip { ); } - /// Add a untargeted event function to this [`AnimationClip`]. + /// Add an event function with no [`AnimationTarget`] to this [`AnimationClip`]. /// /// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds) /// is reached in the animation. /// - /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event`]. + /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event`]. /// See also [`add_event_to_target`](Self::add_event_to_target). /// /// ``` /// # use bevy_animation::AnimationClip; /// # let mut clip = AnimationClip::default(); /// clip.add_event_fn(1.0, |commands, entity, time, weight| { - /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); + /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}"); /// }) /// ``` pub fn add_event_fn( @@ -388,14 +388,14 @@ impl AnimationClip { /// The `func` will trigger on the entity matching the target once the `time` (in seconds) /// is reached in the animation. /// - /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event_to_target`]. + /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event_to_target`]. /// Use [`add_event`](Self::add_event) instead if you don't have a specific target. /// /// ``` /// # use bevy_animation::{AnimationClip, AnimationTargetId}; /// # let mut clip = AnimationClip::default(); /// clip.add_event_fn_to_target(AnimationTargetId::from_iter(["Arm", "Hand"]), 1.0, |commands, entity, time, weight| { - /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); + /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}"); /// }) /// ``` pub fn add_event_fn_to_target( @@ -1534,7 +1534,7 @@ mod tests { use super::*; - #[derive(Event, Reflect, Clone)] + #[derive(Event, EntityEvent, Reflect, Clone)] struct A; #[track_caller] diff --git a/crates/bevy_anti_aliasing/Cargo.toml b/crates/bevy_anti_aliasing/Cargo.toml index c54608a883..8c32d70fff 100644 --- a/crates/bevy_anti_aliasing/Cargo.toml +++ b/crates/bevy_anti_aliasing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_anti_aliasing" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides various anti aliasing implementations for Bevy Engine" homepage = "https://bevy.org" @@ -16,17 +16,17 @@ smaa_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } # other tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs index 0b4a99fb59..d1de3f4cea 100644 --- a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs @@ -3,7 +3,7 @@ use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_image::BevyDefault as _; @@ -163,7 +163,8 @@ impl Plugin for CasPlugin { pub struct CasPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } impl FromWorld for CasPipeline { @@ -187,7 +188,11 @@ impl FromWorld for CasPipeline { CasPipeline { texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "robust_contrast_adaptive_sharpening.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!( + render_world, + "robust_contrast_adaptive_sharpening.wgsl" + ), } } } @@ -209,9 +214,9 @@ impl SpecializedRenderPipeline for CasPipeline { RenderPipelineDescriptor { label: Some("contrast_adaptive_sharpening".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_anti_aliasing/src/fxaa/mod.rs b/crates/bevy_anti_aliasing/src/fxaa/mod.rs index 6b914c4e86..adc2a3d5a2 100644 --- a/crates/bevy_anti_aliasing/src/fxaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/fxaa/mod.rs @@ -3,7 +3,7 @@ use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; use bevy_ecs::prelude::*; use bevy_image::BevyDefault as _; @@ -130,7 +130,8 @@ impl Plugin for FxaaPlugin { pub struct FxaaPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } impl FromWorld for FxaaPipeline { @@ -157,7 +158,8 @@ impl FromWorld for FxaaPipeline { FxaaPipeline { texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "fxaa.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "fxaa.wgsl"), } } } @@ -181,9 +183,9 @@ impl SpecializedRenderPipeline for FxaaPipeline { RenderPipelineDescriptor { label: Some("fxaa".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![ format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(), format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(), diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_aliasing/src/smaa/mod.rs index 0947abc5f8..bb082c5a01 100644 --- a/crates/bevy_anti_aliasing/src/smaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/smaa/mod.rs @@ -847,7 +847,7 @@ impl ViewNode for SmaaNode { view_smaa_uniform_offset, smaa_textures, view_smaa_bind_groups, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_aliasing/src/taa/mod.rs index 0f706146b1..6a00e9c0cf 100644 --- a/crates/bevy_anti_aliasing/src/taa/mod.rs +++ b/crates/bevy_anti_aliasing/src/taa/mod.rs @@ -2,9 +2,9 @@ use bevy_app::{App, Plugin}; use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, prelude::Camera3d, prepass::{DepthPrepass, MotionVectorPrepass, ViewPrepassTextures}, + FullscreenShader, }; use bevy_diagnostic::FrameCount; use bevy_ecs::{ @@ -238,7 +238,8 @@ struct TaaPipeline { taa_bind_group_layout: BindGroupLayout, nearest_sampler: Sampler, linear_sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } impl FromWorld for TaaPipeline { @@ -283,7 +284,8 @@ impl FromWorld for TaaPipeline { taa_bind_group_layout, nearest_sampler, linear_sampler, - shader: load_embedded_asset!(world, "taa.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "taa.wgsl"), } } } @@ -314,9 +316,9 @@ impl SpecializedRenderPipeline for TaaPipeline { RenderPipelineDescriptor { label: Some("taa_pipeline".into()), layout: vec![self.taa_bind_group_layout.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "taa".into(), targets: vec![ diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 6b6120f182..a0c5222b0b 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_app" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides core App functionality for Bevy Engine" homepage = "https://bevy.org" @@ -79,12 +79,12 @@ hotpatching = [ [dependencies] # bevy -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other downcast-rs = { version = "2", default-features = false } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 2adf6c2857..05f3de27b1 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -106,10 +106,13 @@ impl Default for App { #[cfg(feature = "bevy_reflect")] { + use bevy_ecs::observer::ObservedBy; + app.init_resource::(); app.register_type::(); app.register_type::(); app.register_type::(); + app.register_type::(); } #[cfg(feature = "reflect_functions")] @@ -341,7 +344,7 @@ impl App { self } - /// Initializes `T` event handling by inserting an event queue resource ([`Events::`]) + /// Initializes [`BufferedEvent`] handling for `T` by inserting an event queue resource ([`Events::`]) /// and scheduling an [`event_update_system`] in [`First`]. /// /// See [`Events`] for information on how to define events. @@ -352,7 +355,7 @@ impl App { /// # use bevy_app::prelude::*; /// # use bevy_ecs::prelude::*; /// # - /// # #[derive(Event)] + /// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # let mut app = App::new(); /// # @@ -360,7 +363,7 @@ impl App { /// ``` pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { self.main_mut().add_event::(); self @@ -1306,7 +1309,7 @@ impl App { /// Spawns an [`Observer`] entity, which will watch for and respond to the given event. /// - /// `observer` can be any system whose first parameter is a [`Trigger`]. + /// `observer` can be any system whose first parameter is [`On`]. /// /// # Examples /// @@ -1322,14 +1325,14 @@ impl App { /// # friends_allowed: bool, /// # }; /// # - /// # #[derive(Event)] + /// # #[derive(Event, EntityEvent)] /// # struct Invite; /// # /// # #[derive(Component)] /// # struct Friend; /// # /// - /// app.add_observer(|trigger: Trigger, friends: Query>, mut commands: Commands| { + /// app.add_observer(|trigger: On, friends: Query>, mut commands: Commands| { /// if trigger.event().friends_allowed { /// for friend in friends.iter() { /// commands.trigger_targets(Invite, friend); @@ -1404,7 +1407,7 @@ fn run_once(mut app: App) -> AppExit { app.should_exit().unwrap_or(AppExit::Success) } -/// An event that indicates the [`App`] should exit. If one or more of these are present at the end of an update, +/// A [`BufferedEvent`] that indicates the [`App`] should exit. If one or more of these are present at the end of an update, /// the [runner](App::set_runner) will end and ([maybe](App::run)) return control to the caller. /// /// This event can be used to detect when an exit is requested. Make sure that systems listening @@ -1414,7 +1417,7 @@ fn run_once(mut app: App) -> AppExit { /// This type is roughly meant to map to a standard definition of a process exit code (0 means success, not 0 means error). Due to portability concerns /// (see [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html) and [`process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html#)) /// we only allow error codes between 1 and [255](u8::MAX). -#[derive(Event, Debug, Clone, Default, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Default, PartialEq, Eq)] pub enum AppExit { /// [`App`] exited without any problems. #[default] @@ -1482,7 +1485,7 @@ mod tests { change_detection::{DetectChanges, ResMut}, component::Component, entity::Entity, - event::{Event, EventWriter, Events}, + event::{BufferedEvent, Event, EventWriter, Events}, lifecycle::RemovedComponents, query::With, resource::Resource, @@ -1579,7 +1582,7 @@ mod tests { app.add_systems(EnterMainMenu, (foo, bar)); app.world_mut().run_schedule(EnterMainMenu); - assert_eq!(app.world().entities().len(), 2); + assert_eq!(app.world().entity_count(), 2); } #[test] @@ -1848,7 +1851,7 @@ mod tests { } #[test] fn events_should_be_updated_once_per_update() { - #[derive(Event, Clone)] + #[derive(Event, BufferedEvent, Clone)] struct TestEvent; let mut app = App::new(); diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs index 754ba3140e..c6ac5139b9 100644 --- a/crates/bevy_app/src/propagate.rs +++ b/crates/bevy_app/src/propagate.rs @@ -88,6 +88,7 @@ impl core::fmt::Debug for PropagateSet { } impl Eq for PropagateSet {} + impl core::hash::Hash for PropagateSet { fn hash(&self, state: &mut H) { self._p.hash(state); diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index c340b80654..56d6b43d38 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -338,7 +338,7 @@ impl SubApp { /// See [`App::add_event`]. pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { if !self.world.contains_resource::>() { EventRegistry::register_event::(self.world_mut()); diff --git a/crates/bevy_app/src/task_pool_plugin.rs b/crates/bevy_app/src/task_pool_plugin.rs index 5ed4e3fa5d..8014790f07 100644 --- a/crates/bevy_app/src/task_pool_plugin.rs +++ b/crates/bevy_app/src/task_pool_plugin.rs @@ -160,7 +160,7 @@ impl TaskPoolOptions { pub fn create_default_pools(&self) { let total_threads = bevy_tasks::available_parallelism() .clamp(self.min_total_threads, self.max_total_threads); - trace!("Assigning {} cores to default task pools", total_threads); + trace!("Assigning {total_threads} cores to default task pools"); let mut remaining_threads = total_threads; @@ -170,7 +170,7 @@ impl TaskPoolOptions { .io .get_number_of_threads(remaining_threads, total_threads); - trace!("IO Threads: {}", io_threads); + trace!("IO Threads: {io_threads}"); remaining_threads = remaining_threads.saturating_sub(io_threads); IoTaskPool::get_or_init(|| { @@ -200,7 +200,7 @@ impl TaskPoolOptions { .async_compute .get_number_of_threads(remaining_threads, total_threads); - trace!("Async Compute Threads: {}", async_compute_threads); + trace!("Async Compute Threads: {async_compute_threads}"); remaining_threads = remaining_threads.saturating_sub(async_compute_threads); AsyncComputeTaskPool::get_or_init(|| { @@ -231,7 +231,7 @@ impl TaskPoolOptions { .compute .get_number_of_threads(remaining_threads, total_threads); - trace!("Compute Threads: {}", compute_threads); + trace!("Compute Threads: {compute_threads}"); ComputeTaskPool::get_or_init(|| { let builder = TaskPoolBuilder::default() diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index cbb138b0f5..edf8986130 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_asset" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides asset functionality for Bevy Engine" homepage = "https://bevy.org" @@ -19,19 +19,19 @@ watch = [] trace = [] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_asset_macros = { path = "macros", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_asset_macros = { path = "macros", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "uuid", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "async_executor", ] } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -54,10 +54,10 @@ parking_lot = { version = "0.12", default-features = false, features = [ "arc_lock", "send_guard", ] } -ron = { version = "0.8", default-features = false } +ron = { version = "0.10", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } uuid = { version = "1.13.1", default-features = false, features = [ "v4", "serde", @@ -65,7 +65,7 @@ uuid = { version = "1.13.1", default-features = false, features = [ tracing = { version = "0.1", default-features = false } [target.'cfg(target_os = "android")'.dependencies] -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. @@ -78,13 +78,13 @@ web-sys = { version = "0.3", features = [ wasm-bindgen-futures = "0.4" js-sys = "0.3" uuid = { version = "1.13.1", default-features = false, features = ["js"] } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_asset/macros/Cargo.toml b/crates/bevy_asset/macros/Cargo.toml index 4d99d228d3..0b525b3c1d 100644 --- a/crates/bevy_asset/macros/Cargo.toml +++ b/crates/bevy_asset/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_asset_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_asset" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_asset/macros/src/lib.rs b/crates/bevy_asset/macros/src/lib.rs index 443bd09ab9..a7ea87b752 100644 --- a/crates/bevy_asset/macros/src/lib.rs +++ b/crates/bevy_asset/macros/src/lib.rs @@ -1,6 +1,7 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +//! Macros for deriving asset traits. + use bevy_macro_utils::BevyManifest; use proc_macro::{Span, TokenStream}; use quote::{format_ident, quote}; @@ -12,6 +13,7 @@ pub(crate) fn bevy_asset_path() -> Path { const DEPENDENCY_ATTRIBUTE: &str = "dependency"; +/// Implement the `Asset` trait. #[proc_macro_derive(Asset, attributes(dependency))] pub fn derive_asset(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -30,6 +32,7 @@ pub fn derive_asset(input: TokenStream) -> TokenStream { }) } +/// Implement the `VisitAssetDependencies` trait. #[proc_macro_derive(VisitAssetDependencies, attributes(dependency))] pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index d314fa3fd6..b43a8625e7 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -158,9 +158,9 @@ unsafe impl WorldQuery for AssetChanged { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &Self::State, + state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -201,9 +201,9 @@ unsafe impl WorldQuery for AssetChanged { const IS_DENSE: bool = <&A>::IS_DENSE; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -215,7 +215,11 @@ unsafe impl WorldQuery for AssetChanged { } } - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + table: &'w Table, + ) { if let Some(inner) = &mut fetch.inner { // SAFETY: We delegate to the inner `set_table` for `A` unsafe { @@ -265,6 +269,7 @@ unsafe impl QueryFilter for AssetChanged { #[inline] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -272,7 +277,7 @@ unsafe impl QueryFilter for AssetChanged { fetch.inner.as_mut().is_some_and(|inner| { // SAFETY: We delegate to the inner `fetch` for `A` unsafe { - let handle = <&A>::fetch(inner, entity, table_row); + let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row); fetch.check.has_changed(handle) } }) diff --git a/crates/bevy_asset/src/direct_access_ext.rs b/crates/bevy_asset/src/direct_access_ext.rs index 792d523a30..e7e5b993de 100644 --- a/crates/bevy_asset/src/direct_access_ext.rs +++ b/crates/bevy_asset/src/direct_access_ext.rs @@ -20,6 +20,7 @@ pub trait DirectAssetAccessExt { settings: impl Fn(&mut S) + Send + Sync + 'static, ) -> Handle; } + impl DirectAssetAccessExt for World { /// Insert an asset similarly to [`Assets::add`]. /// diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs index 087cb44b5a..42de19fe44 100644 --- a/crates/bevy_asset/src/event.rs +++ b/crates/bevy_asset/src/event.rs @@ -1,12 +1,12 @@ use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; -use bevy_ecs::event::Event; +use bevy_ecs::event::{BufferedEvent, Event}; use bevy_reflect::Reflect; use core::fmt::Debug; -/// An event emitted when a specific [`Asset`] fails to load. +/// A [`BufferedEvent`] emitted when a specific [`Asset`] fails to load. /// /// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`]. -#[derive(Event, Clone, Debug)] +#[derive(Event, BufferedEvent, Clone, Debug)] pub struct AssetLoadFailedEvent { /// The stable identifier of the asset that failed to load. pub id: AssetId, @@ -24,7 +24,7 @@ impl AssetLoadFailedEvent { } /// An untyped version of [`AssetLoadFailedEvent`]. -#[derive(Event, Clone, Debug)] +#[derive(Event, BufferedEvent, Clone, Debug)] pub struct UntypedAssetLoadFailedEvent { /// The stable identifier of the asset that failed to load. pub id: UntypedAssetId, @@ -44,9 +44,9 @@ impl From<&AssetLoadFailedEvent> for UntypedAssetLoadFailedEvent { } } -/// Events that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. +/// [`BufferedEvent`]s that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. #[expect(missing_docs, reason = "Documenting the id fields is unhelpful.")] -#[derive(Event, Reflect)] +#[derive(Event, BufferedEvent, Reflect)] pub enum AssetEvent { /// Emitted whenever an [`Asset`] is added. Added { id: AssetId }, diff --git a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs index f7fb56be74..06a0791a50 100644 --- a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs +++ b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs @@ -56,6 +56,7 @@ pub(crate) struct EmbeddedEventHandler { dir: Dir, last_event: Option, } + impl FilesystemEventHandler for EmbeddedEventHandler { fn begin(&mut self) { self.last_event = None; diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index f6c44397fc..c49d55ca4a 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -141,16 +141,19 @@ impl EmbeddedAssetRegistry { pub trait GetAssetServer { fn get_asset_server(&self) -> &AssetServer; } + impl GetAssetServer for App { fn get_asset_server(&self) -> &AssetServer { self.world().get_asset_server() } } + impl GetAssetServer for World { fn get_asset_server(&self) -> &AssetServer { self.resource() } } + impl GetAssetServer for AssetServer { fn get_asset_server(&self) -> &AssetServer { self diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index de83f9aef6..35bb28d38c 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -264,7 +264,7 @@ pub struct AssetPlugin { /// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid. /// /// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your -/// app will include scripts or modding support, as it could allow allow arbitrary file +/// app will include scripts or modding support, as it could allow arbitrary file /// access for malicious code. /// /// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved) @@ -272,10 +272,10 @@ pub struct AssetPlugin { pub enum UnapprovedPathMode { /// Unapproved asset loading is allowed. This is strongly discouraged. Allow, - /// Fails to load any asset that is is unapproved, unless an override method is used, like + /// Fails to load any asset that is unapproved, unless an override method is used, like /// [`AssetServer::load_override`]. Deny, - /// Fails to load any asset that is is unapproved. + /// Fails to load any asset that is unapproved. #[default] Forbid, } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 50bbfb5dfc..24405f0657 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -344,7 +344,7 @@ impl<'a> LoadContext<'a> { /// Begins a new labeled asset load. Use the returned [`LoadContext`] to load /// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load. - /// When finished, make sure you call [`LoadContext::add_labeled_asset`] to add the results back to the parent + /// When finished, make sure you call [`LoadContext::add_loaded_labeled_asset`] to add the results back to the parent /// context. /// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add /// the labeled [`LoadContext`] back to the parent context. @@ -360,7 +360,7 @@ impl<'a> LoadContext<'a> { /// # let load_context: LoadContext = panic!(); /// let mut handles = Vec::new(); /// for i in 0..2 { - /// let mut labeled = load_context.begin_labeled_asset(); + /// let labeled = load_context.begin_labeled_asset(); /// handles.push(std::thread::spawn(move || { /// (i.to_string(), labeled.finish(Image::default())) /// })); @@ -385,7 +385,7 @@ impl<'a> LoadContext<'a> { /// [`LoadedAsset`], which is registered under the `label` label. /// /// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the - /// result with [`LoadContext::add_labeled_asset`]. + /// result with [`LoadContext::add_loaded_labeled_asset`]. /// /// See [`AssetPath`] for more on labeled assets. pub fn labeled_asset_scope( diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 5c436c1061..a3148cecb7 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -18,16 +18,16 @@ pub struct ReflectAsset { handle_type_id: TypeId, assets_resource_type_id: TypeId, - get: fn(&World, UntypedHandle) -> Option<&dyn Reflect>, + get: fn(&World, UntypedAssetId) -> Option<&dyn Reflect>, // SAFETY: // - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets` resource mutably // - may only be used to access **at most one** access at once - get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedHandle) -> Option<&mut dyn Reflect>, + get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedAssetId) -> Option<&mut dyn Reflect>, add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, - insert: fn(&mut World, UntypedHandle, &dyn PartialReflect), + insert: fn(&mut World, UntypedAssetId, &dyn PartialReflect), len: fn(&World) -> usize, ids: for<'w> fn(&'w World) -> Box + 'w>, - remove: fn(&mut World, UntypedHandle) -> Option>, + remove: fn(&mut World, UntypedAssetId) -> Option>, } impl ReflectAsset { @@ -42,15 +42,19 @@ impl ReflectAsset { } /// Equivalent of [`Assets::get`] - pub fn get<'w>(&self, world: &'w World, handle: UntypedHandle) -> Option<&'w dyn Reflect> { - (self.get)(world, handle) + pub fn get<'w>( + &self, + world: &'w World, + asset_id: impl Into, + ) -> Option<&'w dyn Reflect> { + (self.get)(world, asset_id.into()) } /// Equivalent of [`Assets::get_mut`] pub fn get_mut<'w>( &self, world: &'w mut World, - handle: UntypedHandle, + asset_id: impl Into, ) -> Option<&'w mut dyn Reflect> { // SAFETY: unique world access #[expect( @@ -58,7 +62,7 @@ impl ReflectAsset { reason = "Use of unsafe `Self::get_unchecked_mut()` function." )] unsafe { - (self.get_unchecked_mut)(world.as_unsafe_world_cell(), handle) + (self.get_unchecked_mut)(world.as_unsafe_world_cell(), asset_id.into()) } } @@ -76,8 +80,8 @@ impl ReflectAsset { /// # let handle_1: UntypedHandle = unimplemented!(); /// # let handle_2: UntypedHandle = unimplemented!(); /// let unsafe_world_cell = world.as_unsafe_world_cell(); - /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_1).unwrap() }; - /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_2).unwrap() }; + /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_1).unwrap() }; + /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_2).unwrap() }; /// // ^ not allowed, two mutable references through the same asset resource, even though the /// // handles are distinct /// @@ -96,10 +100,10 @@ impl ReflectAsset { pub unsafe fn get_unchecked_mut<'w>( &self, world: UnsafeWorldCell<'w>, - handle: UntypedHandle, + asset_id: impl Into, ) -> Option<&'w mut dyn Reflect> { // SAFETY: requirements are deferred to the caller - unsafe { (self.get_unchecked_mut)(world, handle) } + unsafe { (self.get_unchecked_mut)(world, asset_id.into()) } } /// Equivalent of [`Assets::add`] @@ -107,13 +111,22 @@ impl ReflectAsset { (self.add)(world, value) } /// Equivalent of [`Assets::insert`] - pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn PartialReflect) { - (self.insert)(world, handle, value); + pub fn insert( + &self, + world: &mut World, + asset_id: impl Into, + value: &dyn PartialReflect, + ) { + (self.insert)(world, asset_id.into(), value); } /// Equivalent of [`Assets::remove`] - pub fn remove(&self, world: &mut World, handle: UntypedHandle) -> Option> { - (self.remove)(world, handle) + pub fn remove( + &self, + world: &mut World, + asset_id: impl Into, + ) -> Option> { + (self.remove)(world, asset_id.into()) } /// Equivalent of [`Assets::len`] @@ -137,17 +150,17 @@ impl FromType for ReflectAsset { ReflectAsset { handle_type_id: TypeId::of::>(), assets_resource_type_id: TypeId::of::>(), - get: |world, handle| { + get: |world, asset_id| { let assets = world.resource::>(); - let asset = assets.get(&handle.typed_debug_checked()); + let asset = assets.get(asset_id.typed_debug_checked()); asset.map(|asset| asset as &dyn Reflect) }, - get_unchecked_mut: |world, handle| { + get_unchecked_mut: |world, asset_id| { // SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets`, // and must ensure to only have at most one reference to it live at all times. #[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")] let assets = unsafe { world.get_resource_mut::>().unwrap().into_inner() }; - let asset = assets.get_mut(&handle.typed_debug_checked()); + let asset = assets.get_mut(asset_id.typed_debug_checked()); asset.map(|asset| asset as &mut dyn Reflect) }, add: |world, value| { @@ -156,11 +169,11 @@ impl FromType for ReflectAsset { .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`"); assets.add(value).untyped() }, - insert: |world, handle, value| { + insert: |world, asset_id, value| { let mut assets = world.resource_mut::>(); let value: A = FromReflect::from_reflect(value) .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`"); - assets.insert(&handle.typed_debug_checked(), value); + assets.insert(asset_id.typed_debug_checked(), value); }, len: |world| { let assets = world.resource::>(); @@ -170,9 +183,9 @@ impl FromType for ReflectAsset { let assets = world.resource::>(); Box::new(assets.ids().map(AssetId::untyped)) }, - remove: |world, handle| { + remove: |world, asset_id| { let mut assets = world.resource_mut::>(); - let value = assets.remove(&handle.typed_debug_checked()); + let value = assets.remove(asset_id.typed_debug_checked()); value.map(|value| Box::new(value) as Box) }, } @@ -200,7 +213,7 @@ impl FromType for ReflectAsset { /// let reflect_asset = type_registry.get_type_data::(reflect_handle.asset_type_id()).unwrap(); /// /// let handle = reflect_handle.downcast_handle_untyped(handle.as_any()).unwrap(); -/// let value = reflect_asset.get(world, handle).unwrap(); +/// let value = reflect_asset.get(world, &handle).unwrap(); /// println!("{value:?}"); /// } /// ``` @@ -210,6 +223,7 @@ pub struct ReflectHandle { downcast_handle_untyped: fn(&dyn Any) -> Option, typed: fn(UntypedHandle) -> Box, } + impl ReflectHandle { /// The [`TypeId`] of the asset pub fn asset_type_id(&self) -> TypeId { @@ -247,7 +261,7 @@ mod tests { use alloc::{string::String, vec::Vec}; use core::any::TypeId; - use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset, UntypedHandle}; + use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset}; use bevy_app::App; use bevy_ecs::reflect::AppTypeRegistry; use bevy_reflect::Reflect; @@ -281,7 +295,7 @@ mod tests { let handle = reflect_asset.add(app.world_mut(), &value); // struct is a reserved keyword, so we can't use it here let strukt = reflect_asset - .get_mut(app.world_mut(), handle) + .get_mut(app.world_mut(), &handle) .unwrap() .reflect_mut() .as_struct() @@ -294,16 +308,12 @@ mod tests { assert_eq!(reflect_asset.len(app.world()), 1); let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); assert_eq!(ids.len(), 1); + let id = ids[0]; - let fetched_handle = UntypedHandle::Weak(ids[0]); - let asset = reflect_asset - .get(app.world(), fetched_handle.clone_weak()) - .unwrap(); + let asset = reflect_asset.get(app.world(), id).unwrap(); assert_eq!(asset.downcast_ref::().unwrap().field, "edited"); - reflect_asset - .remove(app.world_mut(), fetched_handle) - .unwrap(); + reflect_asset.remove(app.world_mut(), id).unwrap(); assert_eq!(reflect_asset.len(app.world()), 0); } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 2b3898cd54..e5020dba75 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -1931,7 +1931,7 @@ pub enum AssetLoadError { base_path, label, all_labels.len(), - all_labels.iter().map(|l| format!("'{}'", l)).collect::>().join(", "))] + all_labels.iter().map(|l| format!("'{l}'")).collect::>().join(", "))] MissingLabel { base_path: AssetPath<'static>, label: String, @@ -1953,6 +1953,14 @@ impl AssetLoaderError { pub fn path(&self) -> &AssetPath<'static> { &self.path } + + /// The error the loader reported when attempting to load the asset. + /// + /// If you know the type of the error the asset loader returned, you can use + /// [`BevyError::downcast_ref()`] to get it. + pub fn error(&self) -> &BevyError { + &self.error + } } /// An error that occurs while resolving an asset added by `add_async`. diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index aff7f83b37..2ffa62db9d 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_audio" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides audio functionality for Bevy Engine" homepage = "https://bevy.org" @@ -10,30 +10,36 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } # other +# TODO: Remove `coreaudio-sys` dep below when updating `cpal`. rodio = { version = "0.20", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_os = "android")'.dependencies] cpal = { version = "0.15", optional = true } +[target.'cfg(target_vendor = "apple")'.dependencies] +# NOTE: Explicitly depend on this patch version to fix: +# https://github.com/bevyengine/bevy/issues/18893 +coreaudio-sys = { version = "0.2.17", default-features = false } + [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. rodio = { version = "0.20", default-features = false, features = [ "wasm-bindgen", ] } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index 9fc757af44..d02d326501 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -57,6 +57,7 @@ pub struct PlaybackRemoveMarker; pub(crate) struct EarPositions<'w, 's> { pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>, } + impl<'w, 's> EarPositions<'w, 's> { /// Gets a set of transformed ear positions. /// diff --git a/crates/bevy_audio/src/volume.rs b/crates/bevy_audio/src/volume.rs index 1f1f417594..b8c0c776a5 100644 --- a/crates/bevy_audio/src/volume.rs +++ b/crates/bevy_audio/src/volume.rs @@ -344,17 +344,11 @@ mod tests { assert!( db_delta.abs() < 1e-2, - "Expected ~{}dB, got {}dB (delta {})", - db, - db_test, - db_delta + "Expected ~{db}dB, got {db_test}dB (delta {db_delta})", ); assert!( linear_relative_delta.abs() < 1e-3, - "Expected ~{}, got {} (relative delta {})", - linear, - linear_test, - linear_relative_delta + "Expected ~{linear}, got {linear_test} (relative delta {linear_relative_delta})", ); } } @@ -474,15 +468,11 @@ mod tests { match (a, b) { (Decibels(a), Decibels(b)) | (Linear(a), Linear(b)) => assert!( (a - b).abs() < EPSILON, - "Expected {:?} to be approximately equal to {:?}", - a, - b + "Expected {a:?} to be approximately equal to {b:?}", ), (a, b) => assert!( (a.to_decibels() - b.to_decibels()).abs() < EPSILON, - "Expected {:?} to be approximately equal to {:?}", - a, - b + "Expected {a:?} to be approximately equal to {b:?}", ), } } diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index ca7a7a74f5..22ade12709 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_color" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Types for representing and manipulating color values" homepage = "https://bevy.org" @@ -10,17 +10,17 @@ keywords = ["bevy", "color"] rust-version = "1.85.0" [dependencies] -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "curve", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } bytemuck = { version = "1", features = ["derive"] } serde = { version = "1.0", features = [ "derive", ], default-features = false, optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } -wgpu-types = { version = "24", default-features = false, optional = true } +derive_more = { version = "2", default-features = false, features = ["from"] } +wgpu-types = { version = "25", default-features = false, optional = true } encase = { version = "0.10", default-features = false, optional = true } [features] diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index b29fce72ac..1579519274 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -92,7 +92,7 @@ impl Hsla { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Hsla::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index e5f5ecab32..2a3b115bb3 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -96,7 +96,7 @@ impl Lcha { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Lcha::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 91ffe422c7..ba52a519ae 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -92,7 +92,7 @@ impl Oklcha { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Oklcha::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 2d903d2cc4..9b3f158af2 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_core_pipeline" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" authors = [ "Bevy Contributors ", @@ -20,20 +20,20 @@ tonemapping_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs index e2ffe1a6c4..8b6d2593c9 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs @@ -196,6 +196,7 @@ impl RenderAsset for GpuAutoExposureCompensationCurve { source: Self::SourceAsset, _: AssetId, (render_device, render_queue): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let texture = render_device.create_texture_with_data( render_queue, diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 111b6e443b..8dc655e91f 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -10,7 +10,7 @@ use bevy_render::{ RenderApp, }; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use crate::FullscreenShader; /// Adds support for specialized "blit pipelines", which can be used to write one texture to another. pub struct BlitPlugin; @@ -38,7 +38,8 @@ impl Plugin for BlitPlugin { pub struct BlitPipeline { pub texture_bind_group: BindGroupLayout, pub sampler: Sampler, - pub shader: Handle, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, } impl FromWorld for BlitPipeline { @@ -61,7 +62,8 @@ impl FromWorld for BlitPipeline { BlitPipeline { texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "blit.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "blit.wgsl"), } } } @@ -80,9 +82,9 @@ impl SpecializedRenderPipeline for BlitPipeline { RenderPipelineDescriptor { label: Some("blit pipeline".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "fs_main".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index 88da2db0cc..201d5a6cbd 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -1,5 +1,6 @@ +use crate::FullscreenShader; + use super::{Bloom, BLOOM_TEXTURE_FORMAT}; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, @@ -27,8 +28,10 @@ pub struct BloomDownsamplingPipeline { /// Layout with a texture, a sampler, and uniforms pub bind_group_layout: BindGroupLayout, pub sampler: Sampler, - /// The shader asset handle. - pub shader: Handle, + /// The asset handle for the fullscreen vertex shader. + pub fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + pub fragment_shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -81,7 +84,8 @@ impl FromWorld for BloomDownsamplingPipeline { BloomDownsamplingPipeline { bind_group_layout, sampler, - shader: load_embedded_asset!(world, "bloom.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "bloom.wgsl"), } } } @@ -122,9 +126,9 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { .into(), ), layout, - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point, targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 10ffdf9c63..65e51c8472 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -121,7 +121,7 @@ impl ViewNode for BloomNode { bloom_settings, upsampling_pipeline_ids, downsampling_pipeline_ids, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { if bloom_settings.intensity == 0.0 { diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index 195c2eb4c0..435ed037b5 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -227,7 +227,7 @@ impl ExtractComponent for Bloom { type QueryFilter = With; type Out = (Self, BloomUniforms); - fn extract_component((bloom, camera): QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component((bloom, camera): QueryItem<'_, '_, Self::QueryData>) -> Option { match ( camera.physical_viewport_rect(), camera.physical_viewport_size(), diff --git a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs index f381e664a9..c49a9d5b16 100644 --- a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs @@ -1,7 +1,8 @@ +use crate::FullscreenShader; + use super::{ downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_TEXTURE_FORMAT, }; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, @@ -27,8 +28,10 @@ pub struct UpsamplingPipelineIds { #[derive(Resource)] pub struct BloomUpsamplingPipeline { pub bind_group_layout: BindGroupLayout, - /// The shader asset handle. - pub shader: Handle, + /// The asset handle for the fullscreen vertex shader. + pub fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + pub fragment_shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -58,7 +61,8 @@ impl FromWorld for BloomUpsamplingPipeline { BloomUpsamplingPipeline { bind_group_layout, - shader: load_embedded_asset!(world, "bloom.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "bloom.wgsl"), } } } @@ -108,9 +112,9 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { RenderPipelineDescriptor { label: Some("bloom_upsampling_pipeline".into()), layout: vec![self.bind_group_layout.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "upsample".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs index 60f355c115..e8cd0c65c6 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs @@ -31,7 +31,7 @@ impl ViewNode for MainOpaquePass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_phases), Some(alpha_mask_phases)) = ( diff --git a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs index 494d4d0f89..4054283a57 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs @@ -28,7 +28,7 @@ impl ViewNode for MainTransparentPass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(transparent_phases) = diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 9bcb2b4f80..f5314c736d 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -80,6 +80,7 @@ impl From for Camera3dDepthTextureUsage { Self(value.bits()) } } + impl From for TextureUsages { fn from(value: Camera3dDepthTextureUsage) -> Self { Self::from_bits_truncate(value.0) diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index 3b1bc96c90..b19268ac1f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -45,7 +45,7 @@ impl ViewNode for MainOpaquePass3dNode { skybox_pipeline, skybox_bind_group, view_uniform_offset, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_phases), Some(alpha_mask_phases)) = ( diff --git a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs index 0e9465aafa..2cf44a4015 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -1,6 +1,6 @@ use crate::{ - fullscreen_vertex_shader::fullscreen_shader_vertex_state, prepass::{DeferredPrepass, ViewPrepassTextures}, + FullscreenShader, }; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, load_embedded_asset}; @@ -130,6 +130,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { ), ); + let vertex_state = world.resource::().to_vertex_state(); let shader = load_embedded_asset!(world, "copy_deferred_lighting_id.wgsl"); let pipeline_id = @@ -138,7 +139,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { .queue_render_pipeline(RenderPipelineDescriptor { label: Some("copy_deferred_lighting_id_pipeline".into()), layout: vec![layout.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state, fragment: Some(FragmentState { shader, shader_defs: vec![], diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index ffac1eec6d..e786d2a222 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -36,7 +36,7 @@ impl ViewNode for EarlyDeferredGBufferPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { run_deferred_prepass( @@ -74,7 +74,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query; @@ -107,6 +107,7 @@ fn run_deferred_prepass<'w>( render_context: &mut RenderContext<'w>, (camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem< 'w, + '_, ::ViewQuery, >, is_late: bool, diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 38f5e1e796..7e2f52e3fc 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -66,7 +66,7 @@ use crate::{ graph::{Core3d, Node3d}, Camera3d, DEPTH_TEXTURE_SAMPLING_SUPPORTED, }, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; /// A plugin that adds support for the depth of field effect to Bevy. @@ -325,8 +325,10 @@ pub struct DepthOfFieldPipeline { /// The bind group layout shared among all invocations of the depth of field /// shader. global_bind_group_layout: BindGroupLayout, - /// The shader asset handle. - shader: Handle, + /// The asset handle for the fullscreen vertex shader. + fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + fragment_shader: Handle, } impl ViewNode for DepthOfFieldNode { @@ -352,7 +354,7 @@ impl ViewNode for DepthOfFieldNode { view_bind_group_layouts, depth_of_field_uniform_index, auxiliary_dof_texture, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -678,13 +680,15 @@ pub fn prepare_depth_of_field_pipelines( &ViewDepthOfFieldBindGroupLayouts, &Msaa, )>, + fullscreen_shader: Res, asset_server: Res, ) { for (entity, view, depth_of_field, view_bind_group_layouts, msaa) in view_targets.iter() { let dof_pipeline = DepthOfFieldPipeline { view_bind_group_layouts: view_bind_group_layouts.clone(), global_bind_group_layout: global_bind_group_layout.layout.clone(), - shader: load_embedded_asset!(asset_server.as_ref(), "dof.wgsl"), + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "dof.wgsl"), }; // We'll need these two flags to create the `DepthOfFieldPipelineKey`s. @@ -797,12 +801,12 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { label: Some("depth of field pipeline".into()), layout, push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), primitive: default(), depth_stencil: None, multisample: default(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: match key.pass { DofPass::GaussianHorizontal => "gaussian_horizontal".into(), diff --git a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs index fee17d1ec6..de8aa856c9 100644 --- a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs +++ b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs @@ -1,25 +1,40 @@ -use bevy_asset::{weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, Handle}; +use bevy_ecs::{resource::Resource, world::FromWorld}; use bevy_render::{prelude::Shader, render_resource::VertexState}; -pub const FULLSCREEN_SHADER_HANDLE: Handle = - weak_handle!("481fb759-d0b1-4175-8319-c439acde30a2"); +/// A shader that renders to the whole screen. Useful for post-processing. +#[derive(Resource, Clone)] +pub struct FullscreenShader(Handle); -/// uses the [`FULLSCREEN_SHADER_HANDLE`] to output a -/// ```wgsl -/// struct FullscreenVertexOutput { -/// [[builtin(position)]] -/// position: vec4; -/// [[location(0)]] -/// uv: vec2; -/// }; -/// ``` -/// from the vertex shader. -/// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);` -pub fn fullscreen_shader_vertex_state() -> VertexState { - VertexState { - shader: FULLSCREEN_SHADER_HANDLE, - shader_defs: Vec::new(), - entry_point: "fullscreen_vertex_shader".into(), - buffers: Vec::new(), +impl FromWorld for FullscreenShader { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self(load_embedded_asset!(world, "fullscreen.wgsl")) + } +} + +impl FullscreenShader { + /// Gets the raw shader handle. + pub fn shader(&self) -> Handle { + self.0.clone() + } + + /// Creates a [`VertexState`] that uses the [`FullscreenShader`] to output a + /// ```wgsl + /// struct FullscreenVertexOutput { + /// @builtin(position) + /// position: vec4; + /// @location(0) + /// uv: vec2; + /// }; + /// ``` + /// from the vertex shader. + /// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);` + pub fn to_vertex_state(&self) -> VertexState { + VertexState { + shader: self.0.clone(), + shader_defs: Vec::new(), + entry_point: "fullscreen_vertex_shader".into(), + buffers: Vec::new(), + } } } diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 6c6bc7ccc7..3526b3e8fb 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -14,18 +14,20 @@ pub mod core_3d; pub mod deferred; pub mod dof; pub mod experimental; -pub mod fullscreen_vertex_shader; pub mod motion_blur; pub mod msaa_writeback; pub mod oit; pub mod post_process; pub mod prepass; -mod skybox; pub mod tonemapping; pub mod upscaling; +pub use fullscreen_vertex_shader::FullscreenShader; pub use skybox::Skybox; +mod fullscreen_vertex_shader; +mod skybox; + /// The core pipeline prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. @@ -42,7 +44,6 @@ use crate::{ deferred::copy_lighting_id::CopyDeferredLightingIdPlugin, dof::DepthOfFieldPlugin, experimental::mip_generation::MipGenerationPlugin, - fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, post_process::PostProcessingPlugin, @@ -51,8 +52,8 @@ use crate::{ upscaling::UpscalingPlugin, }; use bevy_app::{App, Plugin}; -use bevy_asset::load_internal_asset; -use bevy_render::prelude::Shader; +use bevy_asset::embedded_asset; +use bevy_render::RenderApp; use oit::OrderIndependentTransparencyPlugin; #[derive(Default)] @@ -60,17 +61,13 @@ pub struct CorePipelinePlugin; impl Plugin for CorePipelinePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - FULLSCREEN_SHADER_HANDLE, - "fullscreen_vertex_shader/fullscreen.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "fullscreen_vertex_shader/fullscreen.wgsl"); app.register_type::() .register_type::() .register_type::() .register_type::() + .init_resource::() .add_plugins((Core2dPlugin, Core3dPlugin, CopyDeferredLightingIdPlugin)) .add_plugins(( BlitPlugin, @@ -85,4 +82,11 @@ impl Plugin for CorePipelinePlugin { MipGenerationPlugin, )); } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.init_resource::(); + } } diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index dfd4bca103..9e36e508dc 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -25,7 +25,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget}, }; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use crate::FullscreenShader; use super::MotionBlurUniform; @@ -34,11 +34,16 @@ pub struct MotionBlurPipeline { pub(crate) sampler: Sampler, pub(crate) layout: BindGroupLayout, pub(crate) layout_msaa: BindGroupLayout, - pub(crate) shader: Handle, + pub(crate) fullscreen_shader: FullscreenShader, + pub(crate) fragment_shader: Handle, } impl MotionBlurPipeline { - pub(crate) fn new(render_device: &RenderDevice, shader: Handle) -> Self { + pub(crate) fn new( + render_device: &RenderDevice, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, + ) -> Self { let mb_layout = &BindGroupLayoutEntries::sequential( ShaderStages::FRAGMENT, ( @@ -84,7 +89,8 @@ impl MotionBlurPipeline { sampler, layout, layout_msaa, - shader, + fullscreen_shader, + fragment_shader, } } } @@ -93,8 +99,9 @@ impl FromWorld for MotionBlurPipeline { fn from_world(render_world: &mut bevy_ecs::world::World) -> Self { let render_device = render_world.resource::().clone(); - let shader = load_embedded_asset!(render_world, "motion_blur.wgsl"); - MotionBlurPipeline::new(&render_device, shader) + let fullscreen_shader = render_world.resource::().clone(); + let fragment_shader = load_embedded_asset!(render_world, "motion_blur.wgsl"); + MotionBlurPipeline::new(&render_device, fullscreen_shader, fragment_shader) } } @@ -128,9 +135,9 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { RenderPipelineDescriptor { label: Some("motion_blur_pipeline".into()), layout, - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 8dc51e4ed5..5f82e10599 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -61,7 +61,7 @@ impl ViewNode for MsaaWritebackNode { &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (target, blit_pipeline_id, msaa): QueryItem<'w, Self::ViewQuery>, + (target, blit_pipeline_id, msaa): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { if *msaa == Msaa::Off { diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index fe62d0c9b1..7067e5f83b 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -1,7 +1,4 @@ -use crate::{ - fullscreen_vertex_shader::fullscreen_shader_vertex_state, - oit::OrderIndependentTransparencySettings, -}; +use crate::{oit::OrderIndependentTransparencySettings, FullscreenShader}; use bevy_app::Plugin; use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer}; use bevy_derive::Deref; @@ -156,6 +153,7 @@ pub fn queue_oit_resolve_pipeline( ), With, >, + fullscreen_shader: Res, asset_server: Res, // Store the key with the id to make the clean up logic easier. // This also means it will always replace the entry if the key changes so nothing to clean up. @@ -176,7 +174,12 @@ pub fn queue_oit_resolve_pipeline( } } - let desc = specialize_oit_resolve_pipeline(key, &resolve_pipeline, &asset_server); + let desc = specialize_oit_resolve_pipeline( + key, + &resolve_pipeline, + &fullscreen_shader, + &asset_server, + ); let pipeline_id = pipeline_cache.queue_render_pipeline(desc); commands.entity(e).insert(OitResolvePipelineId(pipeline_id)); @@ -194,6 +197,7 @@ pub fn queue_oit_resolve_pipeline( fn specialize_oit_resolve_pipeline( key: OitResolvePipelineKey, resolve_pipeline: &OitResolvePipeline, + fullscreen_shader: &FullscreenShader, asset_server: &AssetServer, ) -> RenderPipelineDescriptor { let format = if key.hdr { @@ -224,7 +228,7 @@ fn specialize_oit_resolve_pipeline( write_mask: ColorWrites::ALL, })], }), - vertex: fullscreen_shader_vertex_state(), + vertex: fullscreen_shader.to_vertex_state(), primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState::default(), diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index 1ab03c5dfa..f7d2501b41 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -44,7 +44,7 @@ use bevy_utils::prelude::default; use crate::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader, + FullscreenShader, }; /// The handle to the default chromatic aberration lookup texture. @@ -130,8 +130,10 @@ pub struct PostProcessingPipeline { source_sampler: Sampler, /// Specifies how to sample the chromatic aberration gradient. chromatic_aberration_lut_sampler: Sampler, - /// The shader asset handle. - shader: Handle, + /// The asset handle for the fullscreen vertex shader. + fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + fragment_shader: Handle, } /// A key that uniquely identifies a built-in postprocessing pipeline. @@ -308,7 +310,8 @@ impl FromWorld for PostProcessingPipeline { bind_group_layout, source_sampler, chromatic_aberration_lut_sampler, - shader: load_embedded_asset!(world, "post_process.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "post_process.wgsl"), } } } @@ -320,9 +323,9 @@ impl SpecializedRenderPipeline for PostProcessingPipeline { RenderPipelineDescriptor { label: Some("postprocessing".into()), layout: vec![self.bind_group_layout.clone()], - vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "fragment_main".into(), targets: vec![Some(ColorTargetState { @@ -352,7 +355,7 @@ impl ViewNode for PostProcessingNode { &self, _: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>, + (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -485,7 +488,7 @@ impl ExtractComponent for ChromaticAberration { type Out = ChromaticAberration; fn extract_component( - chromatic_aberration: QueryItem<'_, Self::QueryData>, + chromatic_aberration: QueryItem<'_, '_, Self::QueryData>, ) -> Option { // Skip the postprocessing phase entirely if the intensity is zero. if chromatic_aberration.intensity > 0.0 { diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index deea2a5fa8..880e2b6892 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -74,11 +74,16 @@ pub struct MotionVectorPrepass; #[reflect(Component, Default)] pub struct DeferredPrepass; +/// View matrices from the previous frame. +/// +/// Useful for temporal rendering techniques that need access to last frame's camera data. #[derive(Component, ShaderType, Clone)] pub struct PreviousViewData { pub view_from_world: Mat4, pub clip_from_world: Mat4, pub clip_from_view: Mat4, + pub world_from_clip: Mat4, + pub view_from_clip: Mat4, } #[derive(Resource, Default)] diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 04cc1890b0..500cc0a42b 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -36,7 +36,7 @@ impl ViewNode for EarlyPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { run_prepass(graph, render_context, view_query, world, "early prepass") @@ -73,7 +73,7 @@ impl ViewNode for LatePrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - query: QueryItem<'w, Self::ViewQuery>, + query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // We only need a late prepass if we have occlusion culling and indirect @@ -112,7 +112,7 @@ fn run_prepass<'w>( _, _, has_deferred, - ): QueryItem<'w, ::ViewQuery>, + ): QueryItem<'w, '_, ::ViewQuery>, world: &'w World, label: &'static str, ) -> Result<(), NodeRunError> { diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index cb75df2053..51c6934ece 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -113,7 +113,9 @@ impl ExtractComponent for Skybox { type QueryFilter = (); type Out = (Self, SkyboxUniforms); - fn extract_component((skybox, exposure): QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component( + (skybox, exposure): QueryItem<'_, '_, Self::QueryData>, + ) -> Option { let exposure = exposure .map(Exposure::exposure) .unwrap_or_else(|| Exposure::default().exposure()); @@ -123,7 +125,7 @@ impl ExtractComponent for Skybox { SkyboxUniforms { brightness: skybox.brightness * exposure, transform: Transform::from_rotation(skybox.rotation) - .compute_matrix() + .to_matrix() .inverse(), #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] _wasm_padding_8b: 0, diff --git a/crates/bevy_core_pipeline/src/skybox/prepass.rs b/crates/bevy_core_pipeline/src/skybox/prepass.rs index a027f69f93..ad63339d05 100644 --- a/crates/bevy_core_pipeline/src/skybox/prepass.rs +++ b/crates/bevy_core_pipeline/src/skybox/prepass.rs @@ -27,7 +27,7 @@ use crate::{ prepass_target_descriptors, MotionVectorPrepass, NormalPrepass, PreviousViewData, PreviousViewUniforms, }, - Skybox, + FullscreenShader, Skybox, }; /// This pipeline writes motion vectors to the prepass for all [`Skybox`]es. @@ -38,7 +38,8 @@ use crate::{ #[derive(Resource)] pub struct SkyboxPrepassPipeline { bind_group_layout: BindGroupLayout, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// Used to specialize the [`SkyboxPrepassPipeline`]. @@ -73,7 +74,8 @@ impl FromWorld for SkyboxPrepassPipeline { ), ), ), - shader: load_embedded_asset!(world, "skybox_prepass.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "skybox_prepass.wgsl"), } } } @@ -86,7 +88,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { label: Some("skybox_prepass_pipeline".into()), layout: vec![self.bind_group_layout.clone()], push_constant_ranges: vec![], - vertex: crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), primitive: default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -101,7 +103,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { alpha_to_coverage_enabled: false, }, fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "fragment".into(), targets: prepass_target_descriptors(key.normal_prepass, true, false), diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 19ac3ef7e2..7453b2bf19 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -1,4 +1,3 @@ -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, load_embedded_asset, Assets, Handle}; use bevy_ecs::prelude::*; @@ -28,6 +27,8 @@ mod node; use bevy_utils::default; pub use node::TonemappingNode; +use crate::FullscreenShader; + /// 3D LUT (look up table) textures used for tonemapping #[derive(Resource, Clone, ExtractResource)] pub struct TonemappingLuts { @@ -112,7 +113,8 @@ impl Plugin for TonemappingPlugin { pub struct TonemappingPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity. @@ -273,9 +275,9 @@ impl SpecializedRenderPipeline for TonemappingPipeline { RenderPipelineDescriptor { label: Some("tonemapping pipeline".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -319,7 +321,8 @@ impl FromWorld for TonemappingPipeline { TonemappingPipeline { texture_bind_group: tonemap_texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "tonemapping.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "tonemapping.wgsl"), } } } @@ -461,5 +464,6 @@ pub fn lut_placeholder() -> Image { sampler: ImageSampler::Default, texture_view_descriptor: None, asset_usage: RenderAssetUsages::RENDER_WORLD, + copy_on_resize: false, } } diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml new file mode 100644 index 0000000000..e93891d8f7 --- /dev/null +++ b/crates/bevy_core_widgets/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "bevy_core_widgets" +version = "0.17.0-dev" +edition = "2024" +description = "Unstyled common widgets for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ + "bevy_ui_picking_backend", +] } + +# other +accesskit = "0.19" + +[features] +default = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs new file mode 100644 index 0000000000..97b15b878d --- /dev/null +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -0,0 +1,130 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::query::Has; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; +use bevy_ui::{InteractionDisabled, Pressed}; + +/// Headless button widget. This widget maintains a "pressed" state, which is used to +/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` +/// event when the button is un-pressed. +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] +pub struct CoreButton { + /// Optional system to run when the button is clicked, or when the Enter or Space key + /// is pressed while the button is focused. If this field is `None`, the button will + /// emit a `ButtonClicked` event when clicked. + pub on_click: Option, +} + +fn button_on_key_event( + mut trigger: On>, + q_state: Query<(&CoreButton, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, disabled)) = q_state.get(trigger.target()) { + if !disabled { + let event = &trigger.event().input; + if !event.repeat + && event.state == ButtonState::Pressed + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + if let Some(on_click) = bstate.on_click { + trigger.propagate(false); + commands.run_system(on_click); + } + } + } + } +} + +fn button_on_pointer_click( + mut trigger: On>, + mut q_state: Query<(&CoreButton, Has, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if pressed && !disabled { + if let Some(on_click) = bstate.on_click { + commands.run_system(on_click); + } + } + } +} + +fn button_on_pointer_down( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && !pressed { + commands.entity(button).insert(Pressed); + } + } +} + +fn button_on_pointer_up( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_drag_end( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_cancel( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +/// Plugin that adds the observers for the [`CoreButton`] widget. +pub struct CoreButtonPlugin; + +impl Plugin for CoreButtonPlugin { + fn build(&self, app: &mut App) { + app.add_observer(button_on_key_event) + .add_observer(button_on_pointer_down) + .add_observer(button_on_pointer_up) + .add_observer(button_on_pointer_click) + .add_observer(button_on_pointer_drag_end) + .add_observer(button_on_pointer_cancel); + } +} diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs new file mode 100644 index 0000000000..fc12811055 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -0,0 +1,179 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::event::{EntityEvent, Event}; +use bevy_ecs::query::{Has, Without}; +use bevy_ecs::system::{In, ResMut}; +use bevy_ecs::{ + component::Component, + observer::On, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_picking::events::{Click, Pointer}; +use bevy_ui::{Checkable, Checked, InteractionDisabled}; + +/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current +/// state of the checkbox. The `on_change` field is an optional system id that will be run when the +/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is +/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox +/// will update its own [`Checked`] state directly. +/// +/// # Toggle switches +/// +/// The [`CoreCheckbox`] component can be used to implement other kinds of toggle widgets. If you +/// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with +/// the `Switch` role instead of the `Checkbox` role. +#[derive(Component, Debug, Default)] +#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] +pub struct CoreCheckbox { + /// One-shot system that is run when the checkbox state needs to be changed. + pub on_change: Option>>, +} + +fn checkbox_on_key_input( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has), Without>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.target()) { + let event = &ev.event().input; + if event.state == ButtonState::Pressed + && !event.repeat + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + ev.propagate(false); + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +fn checkbox_on_pointer_click( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + // Clicking on a button makes it the focused input, + // and hides the focus ring if it was visible. + if let Some(mut focus) = focus { + focus.0 = Some(ev.target()); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + + ev.propagate(false); + if !disabled { + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +/// Event which can be triggered on a checkbox to set the checked state. This can be used to control +/// the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, SetChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(SetChecked(true), checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct SetChecked(pub bool); + +/// Event which can be triggered on a checkbox to toggle the checked state. This can be used to +/// control the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, ToggleChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(ToggleChecked, checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct ToggleChecked; + +fn checkbox_on_set_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + let will_be_checked = ev.event().0; + if will_be_checked != is_checked { + set_checkbox_state(&mut commands, ev.target(), checkbox, will_be_checked); + } + } +} + +fn checkbox_on_toggle_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } +} + +fn set_checkbox_state( + commands: &mut Commands, + entity: impl Into, + checkbox: &CoreCheckbox, + new_state: bool, +) { + if let Some(on_change) = checkbox.on_change { + commands.run_system_with(on_change, new_state); + } else if new_state { + commands.entity(entity.into()).insert(Checked); + } else { + commands.entity(entity.into()).remove::(); + } +} + +/// Plugin that adds the observers for the [`CoreCheckbox`] widget. +pub struct CoreCheckboxPlugin; + +impl Plugin for CoreCheckboxPlugin { + fn build(&self, app: &mut App) { + app.add_observer(checkbox_on_key_input) + .add_observer(checkbox_on_pointer_click) + .add_observer(checkbox_on_set_checked) + .add_observer(checkbox_on_toggle_checked); + } +} diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs new file mode 100644 index 0000000000..d5dd18fb1a --- /dev/null +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -0,0 +1,213 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::hierarchy::{ChildOf, Children}; +use bevy_ecs::query::Has; +use bevy_ecs::system::In; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_picking::events::{Click, Pointer}; +use bevy_ui::{Checked, InteractionDisabled}; + +/// Headless widget implementation for a "radio button group". This component is used to group +/// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It +/// implements the tab navigation logic and keyboard shortcuts for radio buttons. +/// +/// The [`CoreRadioGroup`] component does not have any state itself, and makes no assumptions about +/// what, if any, value is associated with each radio button, or what Rust type that value might be. +/// Instead, the output of the group is the entity id of the selected button. The app can then +/// derive the selected value from this using app-specific means, such as accessing a component on +/// the individual buttons. +/// +/// The [`CoreRadioGroup`] doesn't actually set the [`Checked`] states directly, that is presumed to +/// happen by the app or via some external data-binding scheme. Typically, each button would be +/// associated with a particular constant value, and would be checked whenever that value is equal +/// to the group's value. This also means that as long as each button's associated value is unique +/// within the group, it should never be the case that more than one button is selected at a time. +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))] +pub struct CoreRadioGroup { + /// Callback which is called when the selected radio button changes. + pub on_change: Option>>, +} + +/// Headless widget implementation for radio buttons. These should be enclosed within a +/// [`CoreRadioGroup`] widget, which is responsible for the mutual exclusion logic. +/// +/// According to the WAI-ARIA best practices document, radio buttons should not be focusable, +/// but rather the enclosing group should be focusable. +/// See / +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checked)] +pub struct CoreRadio; + +fn radio_group_on_key_input( + mut ev: On>, + q_group: Query<&CoreRadioGroup>, + q_radio: Query<(Has, Has), With>, + q_children: Query<&Children>, + mut commands: Commands, +) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) { + let event = &ev.event().input; + if event.state == ButtonState::Pressed + && !event.repeat + && matches!( + event.key_code, + KeyCode::ArrowUp + | KeyCode::ArrowDown + | KeyCode::ArrowLeft + | KeyCode::ArrowRight + | KeyCode::Home + | KeyCode::End + ) + { + let key_code = event.key_code; + ev.propagate(false); + + // Find all radio descendants that are not disabled + let radio_buttons = q_children + .iter_descendants(ev.target()) + .filter_map(|child_id| match q_radio.get(child_id) { + Ok((checked, false)) => Some((child_id, checked)), + Ok((_, true)) | Err(_) => None, + }) + .collect::>(); + if radio_buttons.is_empty() { + return; // No enabled radio buttons in the group + } + let current_index = radio_buttons + .iter() + .position(|(_, checked)| *checked) + .unwrap_or(usize::MAX); // Default to invalid index if none are checked + + let next_index = match key_code { + KeyCode::ArrowUp | KeyCode::ArrowLeft => { + // Navigate to the previous radio button in the group + if current_index == 0 || current_index >= radio_buttons.len() { + // If we're at the first one, wrap around to the last + radio_buttons.len() - 1 + } else { + // Move to the previous one + current_index - 1 + } + } + KeyCode::ArrowDown | KeyCode::ArrowRight => { + // Navigate to the next radio button in the group + if current_index >= radio_buttons.len() - 1 { + // If we're at the last one, wrap around to the first + 0 + } else { + // Move to the next one + current_index + 1 + } + } + KeyCode::Home => { + // Navigate to the first radio button in the group + 0 + } + KeyCode::End => { + // Navigate to the last radio button in the group + radio_buttons.len() - 1 + } + _ => { + return; + } + }; + + if current_index == next_index { + // If the next index is the same as the current, do nothing + return; + } + + let (next_id, _) = radio_buttons[next_index]; + + // Trigger the on_change event for the newly checked radio button + if let Some(on_change) = on_change { + commands.run_system_with(*on_change, next_id); + } + } + } +} + +fn radio_group_on_button_click( + mut ev: On>, + q_group: Query<&CoreRadioGroup>, + q_radio: Query<(Has, Has), With>, + q_parents: Query<&ChildOf>, + q_children: Query<&Children>, + mut commands: Commands, +) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) { + // Starting with the original target, search upward for a radio button. + let radio_id = if q_radio.contains(ev.original_target()) { + ev.original_target() + } else { + // Search ancestors for the first radio button + let mut found_radio = None; + for ancestor in q_parents.iter_ancestors(ev.original_target()) { + if q_group.contains(ancestor) { + // We reached a radio group before finding a radio button, bail out + return; + } + if q_radio.contains(ancestor) { + found_radio = Some(ancestor); + break; + } + } + + match found_radio { + Some(radio) => radio, + None => return, // No radio button found in the ancestor chain + } + }; + + // Gather all the enabled radio group descendants for exclusion. + let radio_buttons = q_children + .iter_descendants(ev.target()) + .filter_map(|child_id| match q_radio.get(child_id) { + Ok((checked, false)) => Some((child_id, checked)), + Ok((_, true)) | Err(_) => None, + }) + .collect::>(); + + if radio_buttons.is_empty() { + return; // No enabled radio buttons in the group + } + + // Pick out the radio button that is currently checked. + ev.propagate(false); + let current_radio = radio_buttons + .iter() + .find(|(_, checked)| *checked) + .map(|(id, _)| *id); + + if current_radio == Some(radio_id) { + // If they clicked the currently checked radio button, do nothing + return; + } + + // Trigger the on_change event for the newly checked radio button + if let Some(on_change) = on_change { + commands.run_system_with(*on_change, radio_id); + } + } +} + +/// Plugin that adds the observers for the [`CoreRadioGroup`] widget. +pub struct CoreRadioGroupPlugin; + +impl Plugin for CoreRadioGroupPlugin { + fn build(&self, app: &mut App) { + app.add_observer(radio_group_on_key_input) + .add_observer(radio_group_on_button_click); + } +} diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs new file mode 100644 index 0000000000..d85f12dd22 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -0,0 +1,488 @@ +use core::ops::RangeInclusive; + +use accesskit::{Orientation, Role}; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::event::{EntityEvent, Event}; +use bevy_ecs::hierarchy::Children; +use bevy_ecs::lifecycle::Insert; +use bevy_ecs::query::Has; +use bevy_ecs::system::{In, Res}; +use bevy_ecs::world::DeferredWorld; +use bevy_ecs::{ + component::Component, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_log::warn_once; +use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; +use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; + +/// Defines how the slider should behave when you click on the track (not the thumb). +#[derive(Debug, Default, PartialEq, Clone, Copy)] +pub enum TrackClick { + /// Clicking on the track lets you drag to edit the value, just like clicking on the thumb. + #[default] + Drag, + /// Clicking on the track increments or decrements the slider by [`SliderStep`]. + Step, + /// Clicking on the track snaps the value to the clicked position. + Snap, +} + +/// A headless slider widget, which can be used to build custom sliders. Sliders have a value +/// (represented by the [`SliderValue`] component) and a range (represented by [`SliderRange`]). An +/// optional step size can be specified via [`SliderStep`]. +/// +/// You can also control the slider remotely by triggering a [`SetSliderValue`] event on it. This +/// can be useful in a console environment for controlling the value gamepad inputs. +/// +/// The presence of the `on_change` property controls whether the slider uses internal or external +/// state management. If the `on_change` property is `None`, then the slider updates its own state +/// automatically. Otherwise, the `on_change` property contains the id of a one-shot system which is +/// passed the new slider value. In this case, the slider value is not modified, it is the +/// responsibility of the callback to trigger whatever data-binding mechanism is used to update the +/// slider's value. +/// +/// Typically a slider will contain entities representing the "track" and "thumb" elements. The core +/// slider makes no assumptions about the hierarchical structure of these elements, but expects that +/// the thumb will be marked with a [`CoreSliderThumb`] component. +/// +/// The core slider does not modify the visible position of the thumb: that is the responsibility of +/// the stylist. This can be done either in percent or pixel units as desired. To prevent overhang +/// at the ends of the slider, the positioning should take into account the thumb width, by reducing +/// the amount of travel. So for example, in a slider 100px wide, with a thumb that is 10px, the +/// amount of travel is 90px. The core slider's calculations for clicking and dragging assume this +/// is the case, and will reduce the travel by the measured size of the thumb entity, which allows +/// the movement of the thumb to be perfectly synchronized with the movement of the mouse. +/// +/// In cases where overhang is desired for artistic reasons, the thumb may have additional +/// decorative child elements, absolutely positioned, which don't affect the size measurement. +#[derive(Component, Debug, Default)] +#[require( + AccessibilityNode(accesskit::Node::new(Role::Slider)), + CoreSliderDragState, + SliderValue, + SliderRange, + SliderStep +)] +pub struct CoreSlider { + /// Callback which is called when the slider is dragged or the value is changed via other user + /// interaction. If this value is `None`, then the slider will self-update. + pub on_change: Option>>, + /// Set the track-clicking behavior for this slider. + pub track_click: TrackClick, + // TODO: Think about whether we want a "vertical" option. +} + +/// Marker component that identifies which descendant element is the slider thumb. +#[derive(Component, Debug, Default)] +pub struct CoreSliderThumb; + +/// A component which stores the current value of the slider. +#[derive(Component, Debug, Default, PartialEq, Clone, Copy)] +#[component(immutable)] +pub struct SliderValue(pub f32); + +/// A component which represents the allowed range of the slider value. Defaults to 0.0..=1.0. +#[derive(Component, Debug, PartialEq, Clone, Copy)] +#[component(immutable)] +pub struct SliderRange { + start: f32, + end: f32, +} + +impl SliderRange { + /// Creates a new slider range with the given start and end values. + pub fn new(start: f32, end: f32) -> Self { + if end < start { + warn_once!( + "Expected SliderRange::start ({}) <= SliderRange::end ({})", + start, + end + ); + } + Self { start, end } + } + + /// Creates a new slider range from a Rust range. + pub fn from_range(range: RangeInclusive) -> Self { + let (start, end) = range.into_inner(); + Self { start, end } + } + + /// Returns the minimum allowed value for this slider. + pub fn start(&self) -> f32 { + self.start + } + + /// Return a new instance of a `SliderRange` with a new start position. + pub fn with_start(&self, start: f32) -> Self { + Self::new(start, self.end) + } + + /// Returns the maximum allowed value for this slider. + pub fn end(&self) -> f32 { + self.end + } + + /// Return a new instance of a `SliderRange` with a new end position. + pub fn with_end(&self, end: f32) -> Self { + Self::new(self.start, end) + } + + /// Returns the full span of the range (max - min). + pub fn span(&self) -> f32 { + self.end - self.start + } + + /// Returns the center value of the range. + pub fn center(&self) -> f32 { + (self.start + self.end) / 2.0 + } + + /// Constrain a value between the minimum and maximum allowed values for this slider. + pub fn clamp(&self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } + + /// Compute the position of the thumb on the slider, as a value between 0 and 1, taking + /// into account the proportion of the value between the minimum and maximum limits. + pub fn thumb_position(&self, value: f32) -> f32 { + if self.end > self.start { + (value - self.start) / (self.end - self.start) + } else { + 0.5 + } + } +} + +impl Default for SliderRange { + fn default() -> Self { + Self { + start: 0.0, + end: 1.0, + } + } +} + +/// Defines the amount by which to increment or decrement the slider value when using keyboard +/// shorctuts. Defaults to 1.0. +#[derive(Component, Debug, PartialEq, Clone)] +#[component(immutable)] +pub struct SliderStep(pub f32); + +impl Default for SliderStep { + fn default() -> Self { + Self(1.0) + } +} + +/// Component used to manage the state of a slider during dragging. +#[derive(Component, Default)] +pub struct CoreSliderDragState { + /// Whether the slider is currently being dragged. + pub dragging: bool, + + /// The value of the slider when dragging started. + offset: f32, +} + +pub(crate) fn slider_on_pointer_down( + mut trigger: On>, + q_slider: Query<( + &CoreSlider, + &SliderValue, + &SliderRange, + &SliderStep, + &ComputedNode, + &ComputedNodeTarget, + &UiGlobalTransform, + Has, + )>, + q_thumb: Query<&ComputedNode, With>, + q_children: Query<&Children>, + mut commands: Commands, + ui_scale: Res, +) { + if q_thumb.contains(trigger.target()) { + // Thumb click, stop propagation to prevent track click. + trigger.propagate(false); + } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) = + q_slider.get(trigger.target()) + { + // Track click + trigger.propagate(false); + + if disabled { + return; + } + + // Find thumb size by searching descendants for the first entity with CoreSliderThumb + let thumb_size = q_children + .iter_descendants(trigger.target()) + .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .unwrap_or(0.0); + + // Detect track click. + let local_pos = transform.try_inverse().unwrap().transform_point2( + trigger.event().pointer_location.position * node_target.scale_factor() / ui_scale.0, + ); + let track_width = node.size().x - thumb_size; + // Avoid division by zero + let click_val = if track_width > 0. { + local_pos.x * range.span() / track_width + range.center() + } else { + 0. + }; + + // Compute new value from click position + let new_value = range.clamp(match slider.track_click { + TrackClick::Drag => { + return; + } + TrackClick::Step => { + if click_val < value.0 { + value.0 - step.0 + } else { + value.0 + step.0 + } + } + TrackClick::Snap => click_val, + }); + + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } +} + +pub(crate) fn slider_on_drag_start( + mut trigger: On>, + mut q_slider: Query< + ( + &SliderValue, + &mut CoreSliderDragState, + Has, + ), + With, + >, +) { + if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled { + drag.dragging = true; + drag.offset = value.0; + } + } +} + +pub(crate) fn slider_on_drag( + mut trigger: On>, + mut q_slider: Query<( + &ComputedNode, + &CoreSlider, + &SliderRange, + &UiGlobalTransform, + &mut CoreSliderDragState, + Has, + )>, + q_thumb: Query<&ComputedNode, With>, + q_children: Query<&Children>, + mut commands: Commands, + ui_scale: Res, +) { + if let Ok((node, slider, range, transform, drag, disabled)) = q_slider.get_mut(trigger.target()) + { + trigger.propagate(false); + if drag.dragging && !disabled { + let mut distance = trigger.event().distance / ui_scale.0; + distance.y *= -1.; + let distance = transform.transform_vector2(distance); + // Find thumb size by searching descendants for the first entity with CoreSliderThumb + let thumb_size = q_children + .iter_descendants(trigger.target()) + .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .unwrap_or(0.0); + let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0); + let span = range.span(); + let new_value = if span > 0. { + range.clamp(drag.offset + (distance.x * span) / slider_width) + } else { + range.start() + span * 0.5 + }; + + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } + } +} + +pub(crate) fn slider_on_drag_end( + mut trigger: On>, + mut q_slider: Query<(&CoreSlider, &mut CoreSliderDragState)>, +) { + if let Ok((_slider, mut drag)) = q_slider.get_mut(trigger.target()) { + trigger.propagate(false); + if drag.dragging { + drag.dragging = false; + } + } +} + +fn slider_on_key_input( + mut trigger: On>, + q_slider: Query<( + &CoreSlider, + &SliderValue, + &SliderRange, + &SliderStep, + Has, + )>, + mut commands: Commands, +) { + if let Ok((slider, value, range, step, disabled)) = q_slider.get(trigger.target()) { + let event = &trigger.event().input; + if !disabled && event.state == ButtonState::Pressed { + let new_value = match event.key_code { + KeyCode::ArrowLeft => range.clamp(value.0 - step.0), + KeyCode::ArrowRight => range.clamp(value.0 + step.0), + KeyCode::Home => range.start(), + KeyCode::End => range.end(), + _ => { + return; + } + }; + trigger.propagate(false); + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } + } +} + +pub(crate) fn slider_on_insert(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_orientation(Orientation::Horizontal); + } +} + +pub(crate) fn slider_on_insert_value(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let value = entity.get::().unwrap().0; + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_numeric_value(value.into()); + } +} + +pub(crate) fn slider_on_insert_range(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let range = *entity.get::().unwrap(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_min_numeric_value(range.start().into()); + accessibility.set_max_numeric_value(range.end().into()); + } +} + +pub(crate) fn slider_on_insert_step(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let step = entity.get::().unwrap().0; + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_numeric_value_step(step.into()); + } +} + +/// An [`EntityEvent`] that can be triggered on a slider to modify its value (using the `on_change` callback). +/// This can be used to control the slider via gamepad buttons or other inputs. The value will be +/// clamped when the event is processed. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreSlider, SliderRange, SliderValue, SetSliderValue}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a slider +/// let slider = commands.spawn(( +/// CoreSlider::default(), +/// SliderValue(0.5), +/// SliderRange::new(0.0, 1.0), +/// )).id(); +/// +/// // Set to an absolute value +/// commands.trigger_targets(SetSliderValue::Absolute(0.75), slider); +/// +/// // Adjust relatively +/// commands.trigger_targets(SetSliderValue::Relative(-0.25), slider); +/// } +/// ``` +#[derive(Event, EntityEvent, Clone)] +pub enum SetSliderValue { + /// Set the slider value to a specific value. + Absolute(f32), + /// Add a delta to the slider value. + Relative(f32), + /// Add a delta to the slider value, multiplied by the step size. + RelativeStep(f32), +} + +fn slider_on_set_value( + mut trigger: On, + q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>, + mut commands: Commands, +) { + if let Ok((slider, value, range, step)) = q_slider.get(trigger.target()) { + trigger.propagate(false); + let new_value = match trigger.event() { + SetSliderValue::Absolute(new_value) => range.clamp(*new_value), + SetSliderValue::Relative(delta) => range.clamp(value.0 + *delta), + SetSliderValue::RelativeStep(delta) => { + range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default()) + } + }; + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } +} + +/// Plugin that adds the observers for the [`CoreSlider`] widget. +pub struct CoreSliderPlugin; + +impl Plugin for CoreSliderPlugin { + fn build(&self, app: &mut App) { + app.add_observer(slider_on_pointer_down) + .add_observer(slider_on_drag_start) + .add_observer(slider_on_drag_end) + .add_observer(slider_on_drag) + .add_observer(slider_on_key_input) + .add_observer(slider_on_insert) + .add_observer(slider_on_insert_value) + .add_observer(slider_on_insert_range) + .add_observer(slider_on_insert_step) + .add_observer(slider_on_set_value); + } +} diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs new file mode 100644 index 0000000000..ef9f3db51c --- /dev/null +++ b/crates/bevy_core_widgets/src/lib.rs @@ -0,0 +1,45 @@ +//! This crate provides a set of core widgets for Bevy UI, such as buttons, checkboxes, and sliders. +//! These widgets have no inherent styling, it's the responsibility of the user to add styling +//! appropriate for their game or application. +//! +//! # State Management +//! +//! Most of the widgets use external state management: this means that the widgets do not +//! automatically update their own internal state, but instead rely on the app to update the widget +//! state (as well as any other related game state) in response to a change event emitted by the +//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the +//! user interface is showing a live view of dynamic data coming from deeper within the game engine. + +// Note on naming: the `Core` prefix is used on components that would normally be internal to the +// styled/opinionated widgets that use them. Components which are directly exposed to users above +// the widget level, like `SliderValue`, should not have the `Core` prefix. + +mod core_button; +mod core_checkbox; +mod core_radio; +mod core_slider; + +use bevy_app::{App, Plugin}; + +pub use core_button::{CoreButton, CoreButtonPlugin}; +pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; +pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin}; +pub use core_slider::{ + CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue, + SliderRange, SliderStep, SliderValue, TrackClick, +}; + +/// A plugin that registers the observers for all of the core widgets. If you don't want to +/// use all of the widgets, you can import the individual widget plugins instead. +pub struct CoreWidgetsPlugin; + +impl Plugin for CoreWidgetsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + CoreButtonPlugin, + CoreCheckboxPlugin, + CoreRadioGroupPlugin, + CoreSliderPlugin, + )); + } +} diff --git a/crates/bevy_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml index f127dbffb5..f1a1cd44b3 100644 --- a/crates/bevy_derive/Cargo.toml +++ b/crates/bevy_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides derive implementations for Bevy Engine" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.17.0-dev" } quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 2250a35393..ef31767c0e 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_dev_tools" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Collection of developer tools for the Bevy Engine" homepage = "https://bevy.org" @@ -13,25 +13,25 @@ bevy_ci_testing = ["serde", "ron"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.16.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_state = { path = "../bevy_state", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_state = { path = "../bevy_state", version = "0.17.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } -ron = { version = "0.8.0", optional = true } +ron = { version = "0.10", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] diff --git a/crates/bevy_dev_tools/src/ci_testing/config.rs b/crates/bevy_dev_tools/src/ci_testing/config.rs index 6dc601f1cc..01ab4f26cd 100644 --- a/crates/bevy_dev_tools/src/ci_testing/config.rs +++ b/crates/bevy_dev_tools/src/ci_testing/config.rs @@ -37,6 +37,9 @@ pub enum CiTestingEvent { /// Takes a screenshot of the entire screen, and saves the results to /// `screenshot-{current_frame}.png`. Screenshot, + /// Takes a screenshot of the entire screen, saves the results to + /// `screenshot-{current_frame}.png`, and exits once the screenshot is taken. + ScreenshotAndExit, /// Takes a screenshot of the entire screen, and saves the results to /// `screenshot-{name}.png`. NamedScreenshot(String), @@ -49,7 +52,7 @@ pub enum CiTestingEvent { } /// A custom event that can be configured from a configuration file for CI testing. -#[derive(Event)] +#[derive(Event, BufferedEvent)] pub struct CiTestingCustomEvent(pub String); #[cfg(test)] diff --git a/crates/bevy_dev_tools/src/ci_testing/systems.rs b/crates/bevy_dev_tools/src/ci_testing/systems.rs index f9570133c0..ae7a2a774e 100644 --- a/crates/bevy_dev_tools/src/ci_testing/systems.rs +++ b/crates/bevy_dev_tools/src/ci_testing/systems.rs @@ -21,6 +21,19 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { world.send_event(AppExit::Success); info!("Exiting after {} frames. Test successful!", *current_frame); } + CiTestingEvent::ScreenshotAndExit => { + let this_frame = *current_frame; + world.spawn(Screenshot::primary_window()).observe( + move |captured: On, + mut exit_event: EventWriter| { + let path = format!("./screenshot-{this_frame}.png"); + save_to_disk(path)(captured); + info!("Exiting. Test successful!"); + exit_event.write(AppExit::Success); + }, + ); + info!("Took a screenshot at frame {}.", *current_frame); + } CiTestingEvent::Screenshot => { let path = format!("./screenshot-{}.png", *current_frame); world @@ -29,7 +42,7 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { info!("Took a screenshot at frame {}.", *current_frame); } CiTestingEvent::NamedScreenshot(name) => { - let path = format!("./screenshot-{}.png", name); + let path = format!("./screenshot-{name}.png"); world .spawn(Screenshot::primary_window()) .observe(save_to_disk(path)); diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs index 79c1c8fff4..16233cd3dc 100644 --- a/crates/bevy_dev_tools/src/picking_debug.rs +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -5,9 +5,9 @@ use bevy_color::prelude::*; use bevy_ecs::prelude::*; use bevy_picking::backend::HitData; use bevy_picking::hover::HoverMap; -use bevy_picking::pointer::{Location, PointerId, PointerPress}; +use bevy_picking::pointer::{Location, PointerId, PointerInput, PointerLocation, PointerPress}; use bevy_picking::prelude::*; -use bevy_picking::{pointer, PickingSystems}; +use bevy_picking::PickingSystems; use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_text::prelude::*; @@ -91,11 +91,11 @@ impl Plugin for DebugPickingPlugin { ( // This leaves room to easily change the log-level associated // with different events, should that be desired. - log_event_debug::.run_if(DebugPickingMode::is_noisy), + log_event_debug::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, log_pointer_event_debug::, - log_pointer_event_debug::, - log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, log_pointer_event_debug::, log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, @@ -121,7 +121,7 @@ impl Plugin for DebugPickingPlugin { } /// Listen for any event and logs it at the debug level -pub fn log_event_debug(mut events: EventReader) { +pub fn log_event_debug(mut events: EventReader) { for event in events.read() { debug!("{event:?}"); } @@ -214,7 +214,7 @@ pub fn update_debug_data( entity_names: Query, mut pointers: Query<( &PointerId, - &pointer::PointerLocation, + &PointerLocation, &PointerPress, &mut PointerDebug, )>, diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 708f3b9ee1..e930da149a 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_diagnostic" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides diagnostic functionality for Bevy Engine" homepage = "https://bevy.org" @@ -53,12 +53,12 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index a632c1b49a..df195a6122 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -17,11 +17,13 @@ pub struct FrameTimeDiagnosticsPlugin { /// The smoothing factor for the exponential moving average. Usually `2.0 / (history_length + 1.0)`. pub smoothing_factor: f64, } + impl Default for FrameTimeDiagnosticsPlugin { fn default() -> Self { Self::new(DEFAULT_MAX_HISTORY_LENGTH) } } + impl FrameTimeDiagnosticsPlugin { /// Creates a new `FrameTimeDiagnosticsPlugin` with the specified `max_history_length` and a /// reasonable `smoothing_factor`. diff --git a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs index 4175eb395e..f6888ea723 100644 --- a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs @@ -202,7 +202,7 @@ impl LogDiagnosticsPlugin { ) { if state.timer.tick(time.delta()).is_finished() { Self::for_each_diagnostic(&state, &diagnostics, |diagnostic| { - debug!("{:#?}\n", diagnostic); + debug!("{diagnostic:#?}\n"); }); } } diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml index a980a35b10..334ea10f1f 100644 --- a/crates/bevy_dylib/Cargo.toml +++ b/crates/bevy_dylib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_dylib" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Force the Bevy Engine to be dynamically linked for faster linking" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] crate-type = ["dylib"] [dependencies] -bevy_internal = { path = "../bevy_internal", version = "0.16.0-dev", default-features = false } +bevy_internal = { path = "../bevy_internal", version = "0.17.0-dev", default-features = false } [lints] workspace = true diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 27498f58bc..1e94aa5eed 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ecs" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy Engine's entity component system" homepage = "https://bevy.org" @@ -35,7 +35,7 @@ backtrace = ["std"] ## Enables `tracing` integration, allowing spans and other metrics to be reported ## through that framework. -trace = ["std", "dep:tracing"] +trace = ["std", "dep:tracing", "bevy_utils/debug"] ## Enables a more detailed set of traces which may be noisy if left on by default. detailed_trace = ["trace"] @@ -63,9 +63,9 @@ std = [ "bevy_reflect?/std", "bevy_tasks/std", "bevy_utils/parallel", + "bevy_utils/std", "bitflags/std", "concurrent-queue/std", - "disqualified/alloc", "fixedbitset/std", "indexmap/std", "serde?/std", @@ -86,26 +86,25 @@ critical-section = [ hotpatching = ["dep:subsecond"] [dependencies] -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "smallvec", ], default-features = false, optional = true } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_ecs_macros = { path = "macros", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_ecs_macros = { path = "macros", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } bitflags = { version = "2.3", default-features = false } -disqualified = { version = "1.0", default-features = false } fixedbitset = { version = "0.5", default-features = false } serde = { version = "1", default-features = false, features = [ "alloc", "serde_derive", ], optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = [ +derive_more = { version = "2", default-features = false, features = [ "from", "display", "into", diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index ade3866b5d..b085a79c21 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -277,26 +277,24 @@ world.spawn(PlayerBundle { }); ``` -### Events +### Buffered Events -Events offer a communication channel between one or more systems. Events can be sent using the system parameter `EventWriter` and received with `EventReader`. +Buffered events offer a communication channel between one or more systems. +They can be sent using the `EventWriter` system parameter and received with `EventReader`. ```rust use bevy_ecs::prelude::*; -#[derive(Event)] -struct MyEvent { - message: String, +#[derive(Event, BufferedEvent)] +struct Message(String); + +fn writer(mut writer: EventWriter) { + writer.write(Message("Hello!".to_string())); } -fn writer(mut writer: EventWriter) { - writer.write(MyEvent { - message: "hello!".to_string(), - }); -} - -fn reader(mut reader: EventReader) { - for event in reader.read() { +fn reader(mut reader: EventReader) { + for Message(message) in reader.read() { + println!("{}", message); } } ``` @@ -309,39 +307,41 @@ Observers are systems that listen for a "trigger" of a specific `Event`: use bevy_ecs::prelude::*; #[derive(Event)] -struct MyEvent { +struct Speak { message: String } let mut world = World::new(); -world.add_observer(|trigger: Trigger| { - println!("{}", trigger.event().message); +world.add_observer(|trigger: On| { + println!("{}", trigger.message); }); world.flush(); -world.trigger(MyEvent { - message: "hello!".to_string(), +world.trigger(Speak { + message: "Hello!".to_string(), }); ``` -These differ from `EventReader` and `EventWriter` in that they are "reactive". Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. Triggers can trigger other triggers, and they all will be evaluated at the same time! +These differ from `EventReader` and `EventWriter` in that they are "reactive". +Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. +Triggers can trigger other triggers, and they all will be evaluated at the same time! -Events can also be triggered to target specific entities: +If the event is an `EntityEvent`, it can also be triggered to target specific entities: ```rust use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, EntityEvent)] struct Explode; let mut world = World::new(); let entity = world.spawn_empty().id(); -world.add_observer(|trigger: Trigger, mut commands: Commands| { - println!("Entity {} goes BOOM!", trigger.target().unwrap()); - commands.entity(trigger.target().unwrap()).despawn(); +world.add_observer(|trigger: On, mut commands: Commands| { + println!("Entity {} goes BOOM!", trigger.target()); + commands.entity(trigger.target()).despawn(); }); world.flush(); diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index fb01184048..ecdcb31a33 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -1,4 +1,4 @@ -//! In this example a system sends a custom event with a 50/50 chance during any frame. +//! In this example a system sends a custom buffered event with a 50/50 chance during any frame. //! If an event was sent, it will be printed by the console in a receiving system. #![expect(clippy::print_stdout, reason = "Allowed in examples.")] @@ -15,7 +15,7 @@ fn main() { // Create a schedule to store our systems let mut schedule = Schedule::default(); - // Events need to be updated in every frame in order to clear our buffers. + // Buffered events need to be updated every frame in order to clear our buffers. // This update should happen before we use the events. // Here, we use system sets to control the ordering. #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] @@ -37,7 +37,7 @@ fn main() { } // This is our event that we will send and receive in systems -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct MyEvent { pub message: String, pub random_value: f32, diff --git a/crates/bevy_ecs/macros/Cargo.toml b/crates/bevy_ecs/macros/Cargo.toml index 28605a5d67..4f15bd59fd 100644 --- a/crates/bevy_ecs/macros/Cargo.toml +++ b/crates/bevy_ecs/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ecs_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" description = "Bevy ECS Macros" edition = "2024" license = "MIT OR Apache-2.0" @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = { version = "2.0.99", features = ["full", "extra-traits"] } quote = "1.0" diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 6a693f2ce5..040c1b26b6 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -13,11 +13,28 @@ use syn::{ LitStr, Member, Path, Result, Token, Type, Visibility, }; -pub const EVENT: &str = "event"; +pub const EVENT: &str = "entity_event"; pub const AUTO_PROPAGATE: &str = "auto_propagate"; pub const TRAVERSAL: &str = "traversal"; pub fn derive_event(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Send + Sync + 'static }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {} + }) +} + +pub fn derive_entity_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let mut auto_propagate = false; let mut traversal: Type = parse_quote!(()); @@ -49,13 +66,30 @@ pub fn derive_event(input: TokenStream) -> TokenStream { let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { - impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { + impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { type Traversal = #traversal; const AUTO_PROPAGATE: bool = #auto_propagate; } }) } +pub fn derive_buffered_event(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Send + Sync + 'static }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs_path::event::BufferedEvent for #struct_name #type_generics #where_clause {} + }) +} + pub fn derive_resource(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); @@ -94,9 +128,11 @@ pub fn derive_component(input: TokenStream) -> TokenStream { let map_entities = map_entities( &ast.data, + &bevy_ecs_path, Ident::new("this", Span::call_site()), relationship.is_some(), relationship_target.is_some(), + attrs.map_entities ).map(|map_entities_impl| quote! { fn map_entities(this: &mut Self, mapper: &mut M) { use #bevy_ecs_path::entity::MapEntities; @@ -305,10 +341,19 @@ const ENTITIES: &str = "entities"; pub(crate) fn map_entities( data: &Data, + bevy_ecs_path: &Path, self_ident: Ident, is_relationship: bool, is_relationship_target: bool, + map_entities_attr: Option, ) -> Option { + if let Some(map_entities_override) = map_entities_attr { + let map_entities_tokens = map_entities_override.to_token_stream(bevy_ecs_path); + return Some(quote!( + #map_entities_tokens(#self_ident, mapper) + )); + } + match data { Data::Struct(DataStruct { fields, .. }) => { let mut map = Vec::with_capacity(fields.len()); @@ -396,6 +441,7 @@ pub const ON_INSERT: &str = "on_insert"; pub const ON_REPLACE: &str = "on_replace"; pub const ON_REMOVE: &str = "on_remove"; pub const ON_DESPAWN: &str = "on_despawn"; +pub const MAP_ENTITIES: &str = "map_entities"; pub const IMMUTABLE: &str = "immutable"; pub const CLONE_BEHAVIOR: &str = "clone_behavior"; @@ -450,6 +496,56 @@ impl Parse for HookAttributeKind { } } +#[derive(Debug)] +pub(super) enum MapEntitiesAttributeKind { + /// expressions like function or struct names + /// + /// structs will throw compile errors on the code generation so this is safe + Path(ExprPath), + /// When no value is specified + Default, +} + +impl MapEntitiesAttributeKind { + fn from_expr(value: Expr) -> Result { + match value { + Expr::Path(path) => Ok(Self::Path(path)), + // throw meaningful error on all other expressions + _ => Err(syn::Error::new( + value.span(), + [ + "Not supported in this position, please use one of the following:", + "- path to function", + "- nothing to default to MapEntities implementation", + ] + .join("\n"), + )), + } + } + + fn to_token_stream(&self, bevy_ecs_path: &Path) -> TokenStream2 { + match self { + MapEntitiesAttributeKind::Path(path) => path.to_token_stream(), + MapEntitiesAttributeKind::Default => { + quote!( + ::map_entities + ) + } + } + } +} + +impl Parse for MapEntitiesAttributeKind { + fn parse(input: syn::parse::ParseStream) -> Result { + if input.peek(Token![=]) { + input.parse::()?; + input.parse::().and_then(Self::from_expr) + } else { + Ok(Self::Default) + } + } +} + struct Attrs { storage: StorageTy, requires: Option>, @@ -462,6 +558,7 @@ struct Attrs { relationship_target: Option, immutable: bool, clone_behavior: Option, + map_entities: Option, } #[derive(Clone, Copy)] @@ -501,6 +598,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { relationship_target: None, immutable: false, clone_behavior: None, + map_entities: None, }; let mut require_paths = HashSet::new(); @@ -539,6 +637,9 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { } else if nested.path.is_ident(CLONE_BEHAVIOR) { attrs.clone_behavior = Some(nested.value()?.parse()?); Ok(()) + } else if nested.path.is_ident(MAP_ENTITIES) { + attrs.map_entities = Some(nested.input.parse::()?); + Ok(()) } else { Err(nested.error("Unsupported attribute")) } @@ -758,6 +859,11 @@ fn derive_relationship( #relationship_member: entity } } + + #[inline] + fn set_risky(&mut self, entity: Entity) { + self.#relationship_member = entity; + } } })) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 0a6f9b8884..9bc3e5913e 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -16,7 +16,7 @@ use crate::{ use bevy_macro_utils::{derive_label, ensure_no_collision, get_struct_fields, BevyManifest}; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{ parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, ConstParam, Data, DataStruct, DeriveInput, GenericParam, Index, TypeParam, @@ -79,6 +79,8 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let mut field_kind = Vec::with_capacity(named_fields.len()); for field in named_fields { + let mut kind = BundleFieldKind::Component; + for attr in field .attrs .iter() @@ -86,7 +88,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { { if let Err(error) = attr.parse_nested_meta(|meta| { if meta.path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) { - field_kind.push(BundleFieldKind::Ignore); + kind = BundleFieldKind::Ignore; Ok(()) } else { Err(meta.error(format!( @@ -98,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { } } - field_kind.push(BundleFieldKind::Component); + field_kind.push(kind); } let field = named_fields @@ -111,82 +113,33 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { .map(|field| &field.ty) .collect::>(); - let mut field_component_ids = Vec::new(); - let mut field_get_component_ids = Vec::new(); - let mut field_get_components = Vec::new(); - let mut field_from_components = Vec::new(); - let mut field_required_components = Vec::new(); + let mut active_field_types = Vec::new(); + let mut active_field_tokens = Vec::new(); + let mut inactive_field_tokens = Vec::new(); for (((i, field_type), field_kind), field) in field_type .iter() .enumerate() .zip(field_kind.iter()) .zip(field.iter()) { + let field_tokens = match field { + Some(field) => field.to_token_stream(), + None => Index::from(i).to_token_stream(), + }; match field_kind { BundleFieldKind::Component => { - field_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, &mut *ids); - }); - field_required_components.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::register_required_components(components, required_components); - }); - field_get_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids); - }); - match field { - Some(field) => { - field_get_components.push(quote! { - self.#field.get_components(&mut *func); - }); - field_from_components.push(quote! { - #field: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - None => { - let index = Index::from(i); - field_get_components.push(quote! { - self.#index.get_components(&mut *func); - }); - field_from_components.push(quote! { - #index: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - } + active_field_types.push(field_type); + active_field_tokens.push(field_tokens); } - BundleFieldKind::Ignore => { - field_from_components.push(quote! { - #field: ::core::default::Default::default(), - }); - } + BundleFieldKind::Ignore => inactive_field_tokens.push(field_tokens), } } let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let struct_name = &ast.ident; - let from_components = attributes.impl_from_components.then(|| quote! { - // SAFETY: - // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order - #[allow(deprecated)] - unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { - #[allow(unused_variables, non_snake_case)] - unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self - where - __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> - { - Self{ - #(#field_from_components)* - } - } - } - }); - - let attribute_errors = &errors; - - TokenStream::from(quote! { - #(#attribute_errors)* - + let bundle_impl = quote! { // SAFETY: // - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order // - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass @@ -196,27 +149,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { fn component_ids( components: &mut #ecs_path::component::ComponentsRegistrator, ids: &mut impl FnMut(#ecs_path::component::ComponentId) - ){ - #(#field_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(components, ids);)* } fn get_component_ids( components: &#ecs_path::component::Components, ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>) - ){ - #(#field_get_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)* } fn register_required_components( components: &mut #ecs_path::component::ComponentsRegistrator, required_components: &mut #ecs_path::component::RequiredComponents - ){ - #(#field_required_components)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);)* } } + }; - #from_components - + let dynamic_bundle_impl = quote! { #[allow(deprecated)] impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { type Effect = (); @@ -226,9 +179,36 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { self, func: &mut impl FnMut(#ecs_path::component::StorageType, #ecs_path::ptr::OwningPtr<'_>) ) { - #(#field_get_components)* + #(<#active_field_types as #ecs_path::bundle::DynamicBundle>::get_components(self.#active_field_tokens, &mut *func);)* } } + }; + + let from_components_impl = attributes.impl_from_components.then(|| quote! { + // SAFETY: + // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order + #[allow(deprecated)] + unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { + #[allow(unused_variables, non_snake_case)] + unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self + where + __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> + { + Self { + #(#active_field_tokens: <#active_field_types as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func),)* + #(#inactive_field_tokens: ::core::default::Default::default(),)* + } + } + } + }); + + let attribute_errors = &errors; + + TokenStream::from(quote! { + #(#attribute_errors)* + #bundle_impl + #from_components_impl + #dynamic_bundle_impl }) } @@ -240,9 +220,11 @@ pub fn derive_map_entities(input: TokenStream) -> TokenStream { let map_entities_impl = map_entities( &ast.data, + &ecs_path, Ident::new("self", Span::call_site()), false, false, + None, ); let struct_name = &ast.ident; @@ -440,10 +422,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { > #path::system::SystemParamBuilder<#generic_struct> for #builder_name<#(#builder_type_parameters,)*> #where_clause { - fn build(self, world: &mut #path::world::World, meta: &mut #path::system::SystemMeta) -> <#generic_struct as #path::system::SystemParam>::State { + fn build(self, world: &mut #path::world::World) -> <#generic_struct as #path::system::SystemParam>::State { let #builder_name { #(#fields: #field_locals,)* } = self; #state_struct_name { - state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world, meta) + state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world) } } } @@ -472,12 +454,16 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { type State = #state_struct_name<#punctuated_generic_idents>; type Item<'w, 's> = #struct_name #ty_generics; - fn init_state(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self::State { + fn init_state(world: &mut #path::world::World) -> Self::State { #state_struct_name { - state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world, system_meta), + state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world), } } + fn init_access(state: &Self::State, system_meta: &mut #path::system::SystemMeta, component_access_set: &mut #path::query::FilteredAccessSet<#path::component::ComponentId>, world: &mut #path::world::World) { + <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_access(&state.state, system_meta, component_access_set, world); + } + fn apply(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: &mut #path::world::World) { <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::apply(&mut state.state, system_meta, world); } @@ -569,11 +555,23 @@ pub(crate) fn bevy_ecs_path() -> syn::Path { } /// Implement the `Event` trait. -#[proc_macro_derive(Event, attributes(event))] +#[proc_macro_derive(Event)] pub fn derive_event(input: TokenStream) -> TokenStream { component::derive_event(input) } +/// Implement the `EntityEvent` trait. +#[proc_macro_derive(EntityEvent, attributes(entity_event))] +pub fn derive_entity_event(input: TokenStream) -> TokenStream { + component::derive_entity_event(input) +} + +/// Implement the `BufferedEvent` trait. +#[proc_macro_derive(BufferedEvent)] +pub fn derive_buffered_event(input: TokenStream) -> TokenStream { + component::derive_buffered_event(input) +} + /// Implement the `Resource` trait. #[proc_macro_derive(Resource)] pub fn derive_resource(input: TokenStream) -> TokenStream { diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 4e4529e631..12d9c2bf1c 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -74,12 +74,23 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let user_generics = ast.generics.clone(); let (user_impl_generics, user_ty_generics, user_where_clauses) = user_generics.split_for_impl(); let user_generics_with_world = { - let mut generics = ast.generics; + let mut generics = ast.generics.clone(); generics.params.insert(0, parse_quote!('__w)); generics }; let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) = user_generics_with_world.split_for_impl(); + let user_generics_with_world_and_state = { + let mut generics = ast.generics; + generics.params.insert(0, parse_quote!('__w)); + generics.params.insert(1, parse_quote!('__s)); + generics + }; + let ( + user_impl_generics_with_world_and_state, + user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, + ) = user_generics_with_world_and_state.split_for_impl(); let struct_name = ast.ident; let read_only_struct_name = if attributes.is_mutable { @@ -164,13 +175,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { &visibility, &item_struct_name, &field_types, - &user_impl_generics_with_world, + &user_impl_generics_with_world_and_state, &field_attrs, &field_visibilities, &field_idents, &user_ty_generics, - &user_ty_generics_with_world, - user_where_clauses_with_world, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, ); let mutable_world_query_impl = world_query_impl( &path, @@ -199,13 +210,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { &visibility, &read_only_item_struct_name, &read_only_field_types, - &user_impl_generics_with_world, + &user_impl_generics_with_world_and_state, &field_attrs, &field_visibilities, &field_idents, &user_ty_generics, - &user_ty_generics_with_world, - user_where_clauses_with_world, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, ); let readonly_world_query_impl = world_query_impl( &path, @@ -256,11 +267,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { for #read_only_struct_name #user_ty_generics #user_where_clauses { const IS_READ_ONLY: bool = true; type ReadOnly = #read_only_struct_name #user_ty_generics; - type Item<'__w> = #read_only_item_struct_name #user_ty_generics_with_world; + type Item<'__w, '__s> = #read_only_item_struct_name #user_ty_generics_with_world_and_state; - fn shrink<'__wlong: '__wshort, '__wshort>( - item: Self::Item<'__wlong> - ) -> Self::Item<'__wshort> { + fn shrink<'__wlong: '__wshort, '__wshort, '__s>( + item: Self::Item<'__wlong, '__s> + ) -> Self::Item<'__wshort, '__s> { #read_only_item_struct_name { #( #field_idents: <#read_only_field_types>::shrink(item.#field_idents), @@ -278,13 +289,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] - unsafe fn fetch<'__w>( + unsafe fn fetch<'__w, '__s>( + _state: &'__s Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, - ) -> Self::Item<'__w> { + ) -> Self::Item<'__w, '__s> { Self::Item { - #(#field_idents: <#read_only_field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)* + #(#field_idents: <#read_only_field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)* + } + } + } + + impl #user_impl_generics #path::query::ReleaseStateQueryData + for #read_only_struct_name #user_ty_generics #user_where_clauses + // Make these HRTBs with an unused lifetime parameter to allow trivial constraints + // See https://github.com/rust-lang/rust/issues/48214 + where #(for<'__a> #field_types: #path::query::QueryData,)* { + fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> { + Self::Item { + #(#field_idents: <#read_only_field_types>::release_state(_item.#field_idents),)* } } } @@ -301,11 +325,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { for #struct_name #user_ty_generics #user_where_clauses { const IS_READ_ONLY: bool = #is_read_only; type ReadOnly = #read_only_struct_name #user_ty_generics; - type Item<'__w> = #item_struct_name #user_ty_generics_with_world; + type Item<'__w, '__s> = #item_struct_name #user_ty_generics_with_world_and_state; - fn shrink<'__wlong: '__wshort, '__wshort>( - item: Self::Item<'__wlong> - ) -> Self::Item<'__wshort> { + fn shrink<'__wlong: '__wshort, '__wshort, '__s>( + item: Self::Item<'__wlong, '__s> + ) -> Self::Item<'__wshort, '__s> { #item_struct_name { #( #field_idents: <#field_types>::shrink(item.#field_idents), @@ -323,13 +347,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] - unsafe fn fetch<'__w>( + unsafe fn fetch<'__w, '__s>( + _state: &'__s Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, - ) -> Self::Item<'__w> { + ) -> Self::Item<'__w, '__s> { Self::Item { - #(#field_idents: <#field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)* + #(#field_idents: <#field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)* + } + } + } + + impl #user_impl_generics #path::query::ReleaseStateQueryData + for #struct_name #user_ty_generics #user_where_clauses + // Make these HRTBs with an unused lifetime parameter to allow trivial constraints + // See https://github.com/rust-lang/rust/issues/48214 + where #(for<'__a> #field_types: #path::query::ReleaseStateQueryData,)* { + fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> { + Self::Item { + #(#field_idents: <#field_types>::release_state(_item.#field_idents),)* } } } diff --git a/crates/bevy_ecs/macros/src/query_filter.rs b/crates/bevy_ecs/macros/src/query_filter.rs index c7ddb9cc83..5ae2d2325f 100644 --- a/crates/bevy_ecs/macros/src/query_filter.rs +++ b/crates/bevy_ecs/macros/src/query_filter.rs @@ -102,11 +102,12 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { #[allow(unused_variables)] #[inline(always)] unsafe fn filter_fetch<'__w>( + _state: &Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, ) -> bool { - true #(&& <#field_types>::filter_fetch(&mut _fetch.#named_field_idents, _entity, _table_row))* + true #(&& <#field_types>::filter_fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row))* } } }; diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 5c4c0bff01..5a7d164b80 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -10,13 +10,13 @@ pub(crate) fn item_struct( visibility: &Visibility, item_struct_name: &Ident, field_types: &Vec, - user_impl_generics_with_world: &ImplGenerics, + user_impl_generics_with_world_and_state: &ImplGenerics, field_attrs: &Vec>, field_visibilities: &Vec, field_idents: &Vec, user_ty_generics: &TypeGenerics, - user_ty_generics_with_world: &TypeGenerics, - user_where_clauses_with_world: Option<&WhereClause>, + user_ty_generics_with_world_and_state: &TypeGenerics, + user_where_clauses_with_world_and_state: Option<&WhereClause>, ) -> proc_macro2::TokenStream { let item_attrs = quote! { #[doc = concat!( @@ -33,20 +33,20 @@ pub(crate) fn item_struct( Fields::Named(_) => quote! { #derive_macro_call #item_attrs - #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world { - #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w>,)* + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state { + #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w, '__s>,)* } }, Fields::Unnamed(_) => quote! { #derive_macro_call #item_attrs - #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world( - #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w>, )* + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state( + #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w, '__s>, )* ); }, Fields::Unit => quote! { #item_attrs - #visibility type #item_struct_name #user_ty_generics_with_world = #struct_name #user_ty_generics; + #visibility type #item_struct_name #user_ty_generics_with_world_and_state = #struct_name #user_ty_generics; }, } } @@ -79,7 +79,7 @@ pub(crate) fn world_query_impl( #[automatically_derived] #visibility struct #fetch_struct_name #user_impl_generics_with_world #user_where_clauses_with_world { #(#named_field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)* - #marker_name: &'__w (), + #marker_name: &'__w(), } impl #user_impl_generics_with_world Clone for #fetch_struct_name #user_ty_generics_with_world @@ -110,9 +110,9 @@ pub(crate) fn world_query_impl( } } - unsafe fn init_fetch<'__w>( + unsafe fn init_fetch<'__w, '__s>( _world: #path::world::unsafe_world_cell::UnsafeWorldCell<'__w>, - state: &Self::State, + state: &'__s Self::State, _last_run: #path::component::Tick, _this_run: #path::component::Tick, ) -> ::Fetch<'__w> { @@ -133,9 +133,9 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_archetype` for each member that implements `Fetch` #[inline] - unsafe fn set_archetype<'__w>( + unsafe fn set_archetype<'__w, '__s>( _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + _state: &'__s Self::State, _archetype: &'__w #path::archetype::Archetype, _table: &'__w #path::storage::Table ) { @@ -144,9 +144,9 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_table` for each member that implements `Fetch` #[inline] - unsafe fn set_table<'__w>( + unsafe fn set_table<'__w, '__s>( _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + _state: &'__s Self::State, _table: &'__w #path::storage::Table ) { #(<#field_types>::set_table(&mut _fetch.#named_field_idents, &_state.#named_field_idents, _table);)* diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 7369028715..1ecbad16a1 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -691,41 +691,41 @@ impl Archetype { self.flags().contains(ArchetypeFlags::ON_DESPAWN_HOOK) } - /// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer + /// Returns true if any of the components in this archetype have at least one [`Add`] observer /// - /// [`OnAdd`]: crate::lifecycle::OnAdd + /// [`Add`]: crate::lifecycle::Add #[inline] pub fn has_add_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer + /// Returns true if any of the components in this archetype have at least one [`Insert`] observer /// - /// [`OnInsert`]: crate::lifecycle::OnInsert + /// [`Insert`]: crate::lifecycle::Insert #[inline] pub fn has_insert_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer + /// Returns true if any of the components in this archetype have at least one [`Replace`] observer /// - /// [`OnReplace`]: crate::lifecycle::OnReplace + /// [`Replace`]: crate::lifecycle::Replace #[inline] pub fn has_replace_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer + /// Returns true if any of the components in this archetype have at least one [`Remove`] observer /// - /// [`OnRemove`]: crate::lifecycle::OnRemove + /// [`Remove`]: crate::lifecycle::Remove #[inline] pub fn has_remove_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer + /// Returns true if any of the components in this archetype have at least one [`Despawn`] observer /// - /// [`OnDespawn`]: crate::lifecycle::OnDespawn + /// [`Despawn`]: crate::lifecycle::Despawn #[inline] pub fn has_despawn_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER) @@ -960,6 +960,7 @@ impl Index> for Archetypes { &self.archetypes[index.start.0.index()..] } } + impl Index for Archetypes { type Output = Archetype; diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 7d6c0129dc..8efdc60ad9 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -66,7 +66,7 @@ use crate::{ RequiredComponents, StorageType, Tick, }, entity::{Entities, Entity, EntityLocation}, - lifecycle::{ON_ADD, ON_INSERT, ON_REMOVE, ON_REPLACE}, + lifecycle::{ADD, INSERT, REMOVE, REPLACE}, observer::Observers, prelude::World, query::DebugCheckedUnwrap, @@ -550,10 +550,9 @@ impl BundleInfo { // SAFETY: the caller ensures component_id is valid. unsafe { components.get_info_unchecked(id).name() } }) - .collect::>() - .join(", "); + .collect::>(); - panic!("Bundle {bundle_type_name} has duplicate components: {names}"); + panic!("Bundle {bundle_type_name} has duplicate components: {names:?}"); } // handle explicit components @@ -1191,7 +1190,7 @@ impl<'w> BundleInserter<'w> { if insert_mode == InsertMode::Replace { if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, + REPLACE, Some(entity), archetype_after_insert.iter_existing(), caller, @@ -1376,7 +1375,7 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, + ADD, Some(entity), archetype_after_insert.iter_added(), caller, @@ -1394,7 +1393,7 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, + INSERT, Some(entity), archetype_after_insert.iter_inserted(), caller, @@ -1413,7 +1412,7 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, + INSERT, Some(entity), archetype_after_insert.iter_added(), caller, @@ -1567,7 +1566,7 @@ impl<'w> BundleRemover<'w> { }; if self.old_archetype.as_ref().has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, + REPLACE, Some(entity), bundle_components_in_archetype(), caller, @@ -1582,7 +1581,7 @@ impl<'w> BundleRemover<'w> { ); if self.old_archetype.as_ref().has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, + REMOVE, Some(entity), bundle_components_in_archetype(), caller, @@ -1833,7 +1832,7 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, + ADD, Some(entity), bundle_info.iter_contributed_components(), caller, @@ -1848,7 +1847,7 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, + INSERT, Some(entity), bundle_info.iter_contributed_components(), caller, @@ -2386,7 +2385,7 @@ mod tests { #[derive(Resource, Default)] struct Count(u32); world.init_resource::(); - world.add_observer(|_t: Trigger, mut count: ResMut| { + world.add_observer(|_t: On, mut count: ResMut| { count.0 += 1; }); @@ -2398,4 +2397,13 @@ mod tests { assert_eq!(world.resource::().0, 3); } + + #[derive(Bundle)] + #[expect(unused, reason = "tests the output of the derive macro is valid")] + struct Ignore { + #[bundle(ignore)] + foo: i32, + #[bundle(ignore)] + bar: i32, + } } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 83bee583d7..006b738caf 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -230,7 +230,7 @@ pub trait DetectChangesMut: DetectChanges { /// #[derive(Resource, PartialEq, Eq)] /// pub struct Score(u32); /// - /// #[derive(Event, PartialEq, Eq)] + /// #[derive(Event, BufferedEvent, PartialEq, Eq)] /// pub struct ScoreChanged { /// current: u32, /// previous: u32, diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 338050f5fb..615c5903f8 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -24,7 +24,7 @@ use bevy_platform::{ use bevy_ptr::{OwningPtr, UnsafeCellDeref}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -use bevy_utils::TypeIdMap; +use bevy_utils::{prelude::DebugName, TypeIdMap}; use core::{ alloc::Layout, any::{Any, TypeId}, @@ -34,7 +34,6 @@ use core::{ mem::needs_drop, ops::{Deref, DerefMut}, }; -use disqualified::ShortName; use smallvec::SmallVec; use thiserror::Error; @@ -579,6 +578,65 @@ pub trait Component: Send + Sync + 'static { /// items: Vec> /// } /// ``` + /// + /// You might need more specialized logic. A likely cause of this is your component contains collections of entities that + /// don't implement [`MapEntities`](crate::entity::MapEntities). In that case, you can annotate your component with + /// `#[component(map_entities)]`. Using this attribute, you must implement `MapEntities` for the + /// component itself, and this method will simply call that implementation. + /// + /// ``` + /// # use bevy_ecs::{component::Component, entity::{Entity, MapEntities, EntityMapper}}; + /// # use std::collections::HashMap; + /// #[derive(Component)] + /// #[component(map_entities)] + /// struct Inventory { + /// items: HashMap + /// } + /// + /// impl MapEntities for Inventory { + /// fn map_entities(&mut self, entity_mapper: &mut M) { + /// self.items = self.items + /// .drain() + /// .map(|(id, count)|(entity_mapper.get_mapped(id), count)) + /// .collect(); + /// } + /// } + /// # let a = Entity::from_bits(0x1_0000_0001); + /// # let b = Entity::from_bits(0x1_0000_0002); + /// # let mut inv = Inventory { items: Default::default() }; + /// # inv.items.insert(a, 10); + /// # ::map_entities(&mut inv, &mut (a,b)); + /// # assert_eq!(inv.items.get(&b), Some(&10)); + /// ```` + /// + /// Alternatively, you can specify the path to a function with `#[component(map_entities = function_path)]`, similar to component hooks. + /// In this case, the inputs of the function should mirror the inputs to this method, with the second parameter being generic. + /// + /// ``` + /// # use bevy_ecs::{component::Component, entity::{Entity, MapEntities, EntityMapper}}; + /// # use std::collections::HashMap; + /// #[derive(Component)] + /// #[component(map_entities = map_the_map)] + /// // Also works: map_the_map:: or map_the_map::<_> + /// struct Inventory { + /// items: HashMap + /// } + /// + /// fn map_the_map(inv: &mut Inventory, entity_mapper: &mut M) { + /// inv.items = inv.items + /// .drain() + /// .map(|(id, count)|(entity_mapper.get_mapped(id), count)) + /// .collect(); + /// } + /// # let a = Entity::from_bits(0x1_0000_0001); + /// # let b = Entity::from_bits(0x1_0000_0002); + /// # let mut inv = Inventory { items: Default::default() }; + /// # inv.items.insert(a, 10); + /// # ::map_entities(&mut inv, &mut (a,b)); + /// # assert_eq!(inv.items.get(&b), Some(&10)); + /// ```` + /// + /// You can use the turbofish (`::`) to specify parameters when a function is generic, using either M or _ for the type of the mapper parameter. #[inline] fn map_entities(_this: &mut Self, _mapper: &mut E) {} } @@ -598,7 +656,7 @@ mod private { /// `&mut ...`, created while inserted onto an entity. /// In all other ways, they are identical to mutable components. /// This restriction allows hooks to observe all changes made to an immutable -/// component, effectively turning the `OnInsert` and `OnReplace` hooks into a +/// component, effectively turning the `Insert` and `Replace` hooks into a /// `OnMutate` hook. /// This is not practical for mutable components, as the runtime cost of invoking /// a hook for every exclusive reference created would be far too high. @@ -624,6 +682,7 @@ pub trait ComponentMutability: private::Seal + 'static { pub struct Immutable; impl private::Seal for Immutable {} + impl ComponentMutability for Immutable { const MUTABLE: bool = false; } @@ -634,6 +693,7 @@ impl ComponentMutability for Immutable { pub struct Mutable; impl private::Seal for Mutable {} + impl ComponentMutability for Mutable { const MUTABLE: bool = true; } @@ -678,8 +738,8 @@ impl ComponentInfo { /// Returns the name of the current component. #[inline] - pub fn name(&self) -> &str { - &self.descriptor.name + pub fn name(&self) -> DebugName { + self.descriptor.name.clone() } /// Returns `true` if the current component is mutable. @@ -836,7 +896,7 @@ impl SparseSetIndex for ComponentId { /// A value describing a component or resource, which may or may not correspond to a Rust type. #[derive(Clone)] pub struct ComponentDescriptor { - name: Cow<'static, str>, + name: DebugName, // SAFETY: This must remain private. It must match the statically known StorageType of the // associated rust component type if one exists. storage_type: StorageType, @@ -882,7 +942,7 @@ impl ComponentDescriptor { /// Create a new `ComponentDescriptor` for the type `T`. pub fn new() -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), storage_type: T::STORAGE_TYPE, is_send_and_sync: true, type_id: Some(TypeId::of::()), @@ -907,7 +967,7 @@ impl ComponentDescriptor { clone_behavior: ComponentCloneBehavior, ) -> Self { Self { - name: name.into(), + name: name.into().into(), storage_type, is_send_and_sync: true, type_id: None, @@ -923,7 +983,7 @@ impl ComponentDescriptor { /// The [`StorageType`] for resources is always [`StorageType::Table`]. pub fn new_resource() -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), // PERF: `SparseStorage` may actually be a more // reasonable choice as `storage_type` for resources. storage_type: StorageType::Table, @@ -938,7 +998,7 @@ impl ComponentDescriptor { fn new_non_send(storage_type: StorageType) -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), storage_type, is_send_and_sync: false, type_id: Some(TypeId::of::()), @@ -964,8 +1024,8 @@ impl ComponentDescriptor { /// Returns the name of the current component. #[inline] - pub fn name(&self) -> &str { - self.name.as_ref() + pub fn name(&self) -> DebugName { + self.name.clone() } /// Returns whether this component is mutable. @@ -1854,13 +1914,10 @@ impl Components { /// /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] - pub fn get_name<'a>(&'a self, id: ComponentId) -> Option> { + pub fn get_name<'a>(&'a self, id: ComponentId) -> Option { self.components .get(id.0) - .and_then(|info| { - info.as_ref() - .map(|info| Cow::Borrowed(info.descriptor.name())) - }) + .and_then(|info| info.as_ref().map(|info| info.descriptor.name())) .or_else(|| { let queued = self.queued.read().unwrap_or_else(PoisonError::into_inner); // first check components, then resources, then dynamic @@ -2381,12 +2438,12 @@ impl Tick { /// /// Returns `true` if wrapping was performed. Otherwise, returns `false`. #[inline] - pub fn check_tick(&mut self, tick: Tick) -> bool { - let age = tick.relative_to(*self); + pub fn check_tick(&mut self, check: CheckChangeTicks) -> bool { + let age = check.present_tick().relative_to(*self); // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true // so long as this check always runs before that can happen. if age.get() > Self::MAX.get() { - *self = tick.relative_to(Self::MAX); + *self = check.present_tick().relative_to(Self::MAX); true } else { false @@ -2415,16 +2472,16 @@ impl Tick { /// struct CustomSchedule(Schedule); /// /// # let mut world = World::new(); -/// world.add_observer(|tick: Trigger, mut schedule: ResMut| { -/// schedule.0.check_change_ticks(tick.get()); +/// world.add_observer(|check: On, mut schedule: ResMut| { +/// schedule.0.check_change_ticks(*check); /// }); /// ``` #[derive(Debug, Clone, Copy, Event)] pub struct CheckChangeTicks(pub(crate) Tick); impl CheckChangeTicks { - /// Get the `Tick` that can be used as the parameter of [`Tick::check_tick`]. - pub fn get(self) -> Tick { + /// Get the present `Tick` that other ticks get compared to. + pub fn present_tick(self) -> Tick { self.0 } } @@ -2813,13 +2870,13 @@ pub fn enforce_no_required_components_recursion( "Recursive required components detected: {}\nhelp: {}", recursion_check_stack .iter() - .map(|id| format!("{}", ShortName(&components.get_name(*id).unwrap()))) + .map(|id| format!("{}", components.get_name(*id).unwrap().shortname())) .collect::>() .join(" → "), if direct_recursion { format!( "Remove require({}).", - ShortName(&components.get_name(requiree).unwrap()) + components.get_name(requiree).unwrap().shortname() ) } else { "If this is intentional, consider merging the components.".into() @@ -2972,6 +3029,7 @@ impl Default for DefaultCloneBehaviorSpecialization { pub trait DefaultCloneBehaviorBase { fn default_clone_behavior(&self) -> ComponentCloneBehavior; } + impl DefaultCloneBehaviorBase for DefaultCloneBehaviorSpecialization { fn default_clone_behavior(&self) -> ComponentCloneBehavior { ComponentCloneBehavior::Default @@ -2983,6 +3041,7 @@ impl DefaultCloneBehaviorBase for DefaultCloneBehaviorSpecialization { pub trait DefaultCloneBehaviorViaClone { fn default_clone_behavior(&self) -> ComponentCloneBehavior; } + impl DefaultCloneBehaviorViaClone for &DefaultCloneBehaviorSpecialization { fn default_clone_behavior(&self) -> ComponentCloneBehavior { ComponentCloneBehavior::clone::() diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index b124055d16..08da93c261 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -1,12 +1,14 @@ -use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, vec::Vec}; -use bevy_platform::collections::{HashMap, HashSet}; +use alloc::{boxed::Box, collections::VecDeque, vec::Vec}; +use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet}; use bevy_ptr::{Ptr, PtrMut}; +use bevy_utils::prelude::DebugName; use bumpalo::Bump; -use core::any::TypeId; +use core::{any::TypeId, cell::LazyCell, ops::Range}; +use derive_more::derive::From; use crate::{ archetype::Archetype, - bundle::Bundle, + bundle::{Bundle, BundleId, InsertMode}, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, query::DebugCheckedUnwrap, @@ -80,7 +82,7 @@ pub struct ComponentCloneCtx<'a, 'b> { source: Entity, target: Entity, component_info: &'a ComponentInfo, - entity_cloner: &'a mut EntityCloner, + state: &'a mut EntityClonerState, mapper: &'a mut dyn EntityMapper, #[cfg(feature = "bevy_reflect")] type_registry: Option<&'a crate::reflect::AppTypeRegistry>, @@ -104,7 +106,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { bundle_scratch: &'a mut BundleScratch<'b>, entities: &'a Entities, component_info: &'a ComponentInfo, - entity_cloner: &'a mut EntityCloner, + entity_cloner: &'a mut EntityClonerState, mapper: &'a mut dyn EntityMapper, #[cfg(feature = "bevy_reflect")] type_registry: Option<&'a crate::reflect::AppTypeRegistry>, #[cfg(not(feature = "bevy_reflect"))] type_registry: Option<&'a ()>, @@ -119,7 +121,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { entities, mapper, component_info, - entity_cloner, + state: entity_cloner, type_registry, } } @@ -154,7 +156,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// [`RelationshipTarget::LINKED_SPAWN`](crate::relationship::RelationshipTarget::LINKED_SPAWN) will also be cloned. #[inline] pub fn linked_cloning(&self) -> bool { - self.entity_cloner.linked_cloning + self.state.linked_cloning } /// Returns this context's [`EntityMapper`]. @@ -171,7 +173,8 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// - `ComponentId` of component being written does not match expected `ComponentId`. pub fn write_target_component(&mut self, mut component: C) { C::map_entities(&mut component, &mut self.mapper); - let short_name = disqualified::ShortName::of::(); + let debug_name = DebugName::type_name::(); + let short_name = debug_name.shortname(); if self.target_component_written { panic!("Trying to write component '{short_name}' multiple times") } @@ -270,7 +273,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { pub fn queue_entity_clone(&mut self, entity: Entity) { let target = self.entities.reserve_entity(); self.mapper.set_mapped(entity, target); - self.entity_cloner.clone_queue.push_back(entity); + self.state.clone_queue.push_back(entity); } /// Queues a deferred clone operation, which will run with exclusive [`World`] access immediately after calling the clone handler for each component on an entity. @@ -279,13 +282,12 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { &mut self, deferred: impl FnOnce(&mut World, &mut dyn EntityMapper) + 'static, ) { - self.entity_cloner - .deferred_commands - .push_back(Box::new(deferred)); + self.state.deferred_commands.push_back(Box::new(deferred)); } } -/// A configuration determining how to clone entities. This can be built using [`EntityCloner::build`], which +/// A configuration determining how to clone entities. This can be built using [`EntityCloner::build_opt_out`]/ +/// [`opt_in`](EntityCloner::build_opt_in), which /// returns an [`EntityClonerBuilder`]. /// /// After configuration is complete an entity can be cloned using [`Self::clone_entity`]. @@ -306,7 +308,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// let entity = world.spawn(component.clone()).id(); /// let entity_clone = world.spawn_empty().id(); /// -/// EntityCloner::build(&mut world).clone_entity(entity, entity_clone); +/// EntityCloner::build_opt_out(&mut world).clone_entity(entity, entity_clone); /// /// assert!(world.get::(entity_clone).is_some_and(|c| *c == component)); ///``` @@ -338,32 +340,10 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// 2. component-defined handler using [`Component::clone_behavior`] /// 3. default handler override using [`EntityClonerBuilder::with_default_clone_fn`]. /// 4. reflect-based or noop default clone handler depending on if `bevy_reflect` feature is enabled or not. +#[derive(Default)] pub struct EntityCloner { - filter_allows_components: bool, - filter: HashSet, - filter_required: HashSet, - clone_behavior_overrides: HashMap, - move_components: bool, - linked_cloning: bool, - default_clone_fn: ComponentCloneFn, - clone_queue: VecDeque, - deferred_commands: VecDeque>, -} - -impl Default for EntityCloner { - fn default() -> Self { - Self { - filter_allows_components: false, - move_components: false, - linked_cloning: false, - default_clone_fn: ComponentCloneBehavior::global_default_fn(), - filter: Default::default(), - filter_required: Default::default(), - clone_behavior_overrides: Default::default(), - clone_queue: Default::default(), - deferred_commands: Default::default(), - } - } + filter: EntityClonerFilter, + state: EntityClonerState, } /// An expandable scratch space for defining a dynamic bundle. @@ -431,12 +411,33 @@ impl<'a> BundleScratch<'a> { } impl EntityCloner { - /// Returns a new [`EntityClonerBuilder`] using the given `world`. - pub fn build(world: &mut World) -> EntityClonerBuilder { + /// Returns a new [`EntityClonerBuilder`] using the given `world` with the [`OptOut`] configuration. + /// + /// This builder tries to clone every component from the source entity except for components that were + /// explicitly denied, for example by using the [`deny`](EntityClonerBuilder::deny) method. + /// + /// Required components are not considered by denied components and must be explicitly denied as well if desired. + pub fn build_opt_out(world: &mut World) -> EntityClonerBuilder { EntityClonerBuilder { world, - attach_required_components: true, - entity_cloner: EntityCloner::default(), + filter: Default::default(), + state: Default::default(), + } + } + + /// Returns a new [`EntityClonerBuilder`] using the given `world` with the [`OptIn`] configuration. + /// + /// This builder tries to clone every component that was explicitly allowed from the source entity, + /// for example by using the [`allow`](EntityClonerBuilder::allow) method. + /// + /// Components allowed to be cloned through this builder would also allow their required components, + /// which will be cloned from the source entity only if the target entity does not contain them already. + /// To skip adding required components see [`without_required_components`](EntityClonerBuilder::without_required_components). + pub fn build_opt_in(world: &mut World) -> EntityClonerBuilder { + EntityClonerBuilder { + world, + filter: Default::default(), + state: Default::default(), } } @@ -444,116 +445,7 @@ impl EntityCloner { /// This will produce "deep" / recursive clones of relationship trees that have "linked spawn". #[inline] pub fn linked_cloning(&self) -> bool { - self.linked_cloning - } - - /// Clones and inserts components from the `source` entity into the entity mapped by `mapper` from `source` using the stored configuration. - fn clone_entity_internal( - &mut self, - world: &mut World, - source: Entity, - mapper: &mut dyn EntityMapper, - relationship_hook_insert_mode: RelationshipHookMode, - ) -> Entity { - let target = mapper.get_mapped(source); - // PERF: reusing allocated space across clones would be more efficient. Consider an allocation model similar to `Commands`. - let bundle_scratch_allocator = Bump::new(); - let mut bundle_scratch: BundleScratch; - { - let world = world.as_unsafe_world_cell(); - let source_entity = world.get_entity(source).expect("Source entity must exist"); - let target_archetype = (!self.filter_required.is_empty()).then(|| { - world - .get_entity(target) - .expect("Target entity must exist") - .archetype() - }); - - #[cfg(feature = "bevy_reflect")] - // SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone - // the registry, which prevents future conflicts. - let app_registry = unsafe { - world - .get_resource::() - .cloned() - }; - #[cfg(not(feature = "bevy_reflect"))] - let app_registry = Option::<()>::None; - - let archetype = source_entity.archetype(); - bundle_scratch = BundleScratch::with_capacity(archetype.component_count()); - - for component in archetype.components() { - if !self.is_cloning_allowed(&component, target_archetype) { - continue; - } - - let handler = match self.clone_behavior_overrides.get(&component) { - Some(clone_behavior) => clone_behavior.resolve(self.default_clone_fn), - None => world - .components() - .get_info(component) - .map(|info| info.clone_behavior().resolve(self.default_clone_fn)) - .unwrap_or(self.default_clone_fn), - }; - - // SAFETY: This component exists because it is present on the archetype. - let info = unsafe { world.components().get_info_unchecked(component) }; - - // SAFETY: - // - There are no other mutable references to source entity. - // - `component` is from `source_entity`'s archetype - let source_component_ptr = - unsafe { source_entity.get_by_id(component).debug_checked_unwrap() }; - - let source_component = SourceComponent { - info, - ptr: source_component_ptr, - }; - - // SAFETY: - // - `components` and `component` are from the same world - // - `source_component_ptr` is valid and points to the same type as represented by `component` - let mut ctx = unsafe { - ComponentCloneCtx::new( - component, - source, - target, - &bundle_scratch_allocator, - &mut bundle_scratch, - world.entities(), - info, - self, - mapper, - app_registry.as_ref(), - ) - }; - - (handler)(&source_component, &mut ctx); - } - } - - world.flush(); - - for deferred in self.deferred_commands.drain(..) { - (deferred)(world, mapper); - } - - if !world.entities.contains(target) { - panic!("Target entity does not exist"); - } - - if self.move_components { - world - .entity_mut(source) - .remove_by_ids(&bundle_scratch.component_ids); - } - - // SAFETY: - // - All `component_ids` are from the same world as `target` entity - // - All `component_data_ptrs` are valid types represented by `component_ids` - unsafe { bundle_scratch.write(world, target, relationship_hook_insert_mode) }; - target + self.state.linked_cloning } /// Clones and inserts components from the `source` entity into `target` entity using the stored configuration. @@ -585,10 +477,29 @@ impl EntityCloner { world: &mut World, source: Entity, mapper: &mut dyn EntityMapper, + ) -> Entity { + Self::clone_entity_mapped_internal(&mut self.state, &mut self.filter, world, source, mapper) + } + + #[track_caller] + #[inline] + fn clone_entity_mapped_internal( + state: &mut EntityClonerState, + filter: &mut impl CloneByFilter, + world: &mut World, + source: Entity, + mapper: &mut dyn EntityMapper, ) -> Entity { // All relationships on the root should have their hooks run - let target = self.clone_entity_internal(world, source, mapper, RelationshipHookMode::Run); - let child_hook_insert_mode = if self.linked_cloning { + let target = Self::clone_entity_internal( + state, + filter, + world, + source, + mapper, + RelationshipHookMode::Run, + ); + let child_hook_insert_mode = if state.linked_cloning { // When spawning "linked relationships", we want to ignore hooks for relationships we are spawning, while // still registering with original relationship targets that are "not linked" to the current recursive spawn. RelationshipHookMode::RunIfNotLinked @@ -598,9 +509,16 @@ impl EntityCloner { RelationshipHookMode::Run }; loop { - let queued = self.clone_queue.pop_front(); + let queued = state.clone_queue.pop_front(); if let Some(queued) = queued { - self.clone_entity_internal(world, queued, mapper, child_hook_insert_mode); + Self::clone_entity_internal( + state, + filter, + world, + queued, + mapper, + child_hook_insert_mode, + ); } else { break; } @@ -608,58 +526,170 @@ impl EntityCloner { target } - fn is_cloning_allowed( - &self, - component: &ComponentId, - target_archetype: Option<&Archetype>, - ) -> bool { - if self.filter_allows_components { - self.filter.contains(component) - || target_archetype.is_some_and(|archetype| { - !archetype.contains(*component) && self.filter_required.contains(component) - }) - } else { - !self.filter.contains(component) && !self.filter_required.contains(component) + /// Clones and inserts components from the `source` entity into the entity mapped by `mapper` from `source` using the stored configuration. + fn clone_entity_internal( + state: &mut EntityClonerState, + filter: &mut impl CloneByFilter, + world: &mut World, + source: Entity, + mapper: &mut dyn EntityMapper, + relationship_hook_insert_mode: RelationshipHookMode, + ) -> Entity { + let target = mapper.get_mapped(source); + // PERF: reusing allocated space across clones would be more efficient. Consider an allocation model similar to `Commands`. + let bundle_scratch_allocator = Bump::new(); + let mut bundle_scratch: BundleScratch; + { + let world = world.as_unsafe_world_cell(); + let source_entity = world.get_entity(source).expect("Source entity must exist"); + + #[cfg(feature = "bevy_reflect")] + // SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone + // the registry, which prevents future conflicts. + let app_registry = unsafe { + world + .get_resource::() + .cloned() + }; + #[cfg(not(feature = "bevy_reflect"))] + let app_registry = Option::<()>::None; + + let source_archetype = source_entity.archetype(); + bundle_scratch = BundleScratch::with_capacity(source_archetype.component_count()); + + let target_archetype = LazyCell::new(|| { + world + .get_entity(target) + .expect("Target entity must exist") + .archetype() + }); + + filter.clone_components(source_archetype, target_archetype, |component| { + let handler = match state.clone_behavior_overrides.get(&component) { + Some(clone_behavior) => clone_behavior.resolve(state.default_clone_fn), + None => world + .components() + .get_info(component) + .map(|info| info.clone_behavior().resolve(state.default_clone_fn)) + .unwrap_or(state.default_clone_fn), + }; + + // SAFETY: This component exists because it is present on the archetype. + let info = unsafe { world.components().get_info_unchecked(component) }; + + // SAFETY: + // - There are no other mutable references to source entity. + // - `component` is from `source_entity`'s archetype + let source_component_ptr = + unsafe { source_entity.get_by_id(component).debug_checked_unwrap() }; + + let source_component = SourceComponent { + info, + ptr: source_component_ptr, + }; + + // SAFETY: + // - `components` and `component` are from the same world + // - `source_component_ptr` is valid and points to the same type as represented by `component` + let mut ctx = unsafe { + ComponentCloneCtx::new( + component, + source, + target, + &bundle_scratch_allocator, + &mut bundle_scratch, + world.entities(), + info, + state, + mapper, + app_registry.as_ref(), + ) + }; + + (handler)(&source_component, &mut ctx); + }); + } + + world.flush(); + + for deferred in state.deferred_commands.drain(..) { + (deferred)(world, mapper); + } + + if !world.entities.contains(target) { + panic!("Target entity does not exist"); + } + + if state.move_components { + world + .entity_mut(source) + .remove_by_ids(&bundle_scratch.component_ids); + } + + // SAFETY: + // - All `component_ids` are from the same world as `target` entity + // - All `component_data_ptrs` are valid types represented by `component_ids` + unsafe { bundle_scratch.write(world, target, relationship_hook_insert_mode) }; + target + } +} + +/// Part of the [`EntityCloner`], see there for more information. +struct EntityClonerState { + clone_behavior_overrides: HashMap, + move_components: bool, + linked_cloning: bool, + default_clone_fn: ComponentCloneFn, + clone_queue: VecDeque, + deferred_commands: VecDeque>, +} + +impl Default for EntityClonerState { + fn default() -> Self { + Self { + move_components: false, + linked_cloning: false, + default_clone_fn: ComponentCloneBehavior::global_default_fn(), + clone_behavior_overrides: Default::default(), + clone_queue: Default::default(), + deferred_commands: Default::default(), } } } /// A builder for configuring [`EntityCloner`]. See [`EntityCloner`] for more information. -pub struct EntityClonerBuilder<'w> { +pub struct EntityClonerBuilder<'w, Filter> { world: &'w mut World, - entity_cloner: EntityCloner, - attach_required_components: bool, + filter: Filter, + state: EntityClonerState, } -impl<'w> EntityClonerBuilder<'w> { +impl<'w, Filter: CloneByFilter> EntityClonerBuilder<'w, Filter> { /// Internally calls [`EntityCloner::clone_entity`] on the builder's [`World`]. pub fn clone_entity(&mut self, source: Entity, target: Entity) -> &mut Self { - self.entity_cloner.clone_entity(self.world, source, target); + let mut mapper = EntityHashMap::::new(); + mapper.set_mapped(source, target); + EntityCloner::clone_entity_mapped_internal( + &mut self.state, + &mut self.filter, + self.world, + source, + &mut mapper, + ); self } - /// Finishes configuring [`EntityCloner`] returns it. - pub fn finish(self) -> EntityCloner { - self.entity_cloner - } - /// By default, any components allowed/denied through the filter will automatically - /// allow/deny all of their required components. - /// - /// This method allows for a scoped mode where any changes to the filter - /// will not involve required components. - pub fn without_required_components( - &mut self, - builder: impl FnOnce(&mut EntityClonerBuilder), - ) -> &mut Self { - self.attach_required_components = false; - builder(self); - self.attach_required_components = true; - self + /// Finishes configuring [`EntityCloner`] returns it. + pub fn finish(self) -> EntityCloner { + EntityCloner { + filter: self.filter.into(), + state: self.state, + } } /// Sets the default clone function to use. pub fn with_default_clone_fn(&mut self, clone_fn: ComponentCloneFn) -> &mut Self { - self.entity_cloner.default_clone_fn = clone_fn; + self.state.default_clone_fn = clone_fn; self } @@ -671,86 +701,7 @@ impl<'w> EntityClonerBuilder<'w> { /// The setting only applies to components that are allowed through the filter /// at the time [`EntityClonerBuilder::clone_entity`] is called. pub fn move_components(&mut self, enable: bool) -> &mut Self { - self.entity_cloner.move_components = enable; - self - } - - /// Adds all components of the bundle to the list of components to clone. - /// - /// Note that all components are allowed by default, to clone only explicitly allowed components make sure to call - /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. - pub fn allow(&mut self) -> &mut Self { - let bundle = self.world.register_bundle::(); - let ids = bundle.explicit_components().to_owned(); - for id in ids { - self.filter_allow(id); - } - self - } - - /// Extends the list of components to clone. - /// - /// Note that all components are allowed by default, to clone only explicitly allowed components make sure to call - /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. - pub fn allow_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for id in ids { - self.filter_allow(id); - } - self - } - - /// Extends the list of components to clone using [`TypeId`]s. - /// - /// Note that all components are allowed by default, to clone only explicitly allowed components make sure to call - /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. - pub fn allow_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for type_id in ids { - if let Some(id) = self.world.components().get_valid_id(type_id) { - self.filter_allow(id); - } - } - self - } - - /// Resets the filter to allow all components to be cloned. - pub fn allow_all(&mut self) -> &mut Self { - self.entity_cloner.filter_allows_components = false; - self.entity_cloner.filter.clear(); - self - } - - /// Disallows all components of the bundle from being cloned. - pub fn deny(&mut self) -> &mut Self { - let bundle = self.world.register_bundle::(); - let ids = bundle.explicit_components().to_owned(); - for id in ids { - self.filter_deny(id); - } - self - } - - /// Extends the list of components that shouldn't be cloned. - pub fn deny_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for id in ids { - self.filter_deny(id); - } - self - } - - /// Extends the list of components that shouldn't be cloned by type ids. - pub fn deny_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for type_id in ids { - if let Some(id) = self.world.components().get_valid_id(type_id) { - self.filter_deny(id); - } - } - self - } - - /// Sets the filter to deny all components. - pub fn deny_all(&mut self) -> &mut Self { - self.entity_cloner.filter_allows_components = true; - self.entity_cloner.filter.clear(); + self.state.move_components = enable; self } @@ -763,7 +714,7 @@ impl<'w> EntityClonerBuilder<'w> { clone_behavior: ComponentCloneBehavior, ) -> &mut Self { if let Some(id) = self.world.components().valid_component_id::() { - self.entity_cloner + self.state .clone_behavior_overrides .insert(id, clone_behavior); } @@ -779,7 +730,7 @@ impl<'w> EntityClonerBuilder<'w> { component_id: ComponentId, clone_behavior: ComponentCloneBehavior, ) -> &mut Self { - self.entity_cloner + self.state .clone_behavior_overrides .insert(component_id, clone_behavior); self @@ -788,7 +739,7 @@ impl<'w> EntityClonerBuilder<'w> { /// Removes a previously set override of [`ComponentCloneBehavior`] for a component in this builder. pub fn remove_clone_behavior_override(&mut self) -> &mut Self { if let Some(id) = self.world.components().valid_component_id::() { - self.entity_cloner.clone_behavior_overrides.remove(&id); + self.state.clone_behavior_overrides.remove(&id); } self } @@ -798,53 +749,317 @@ impl<'w> EntityClonerBuilder<'w> { &mut self, component_id: ComponentId, ) -> &mut Self { - self.entity_cloner - .clone_behavior_overrides - .remove(&component_id); + self.state.clone_behavior_overrides.remove(&component_id); self } /// When true this cloner will be configured to clone entities referenced in cloned components via [`RelationshipTarget::LINKED_SPAWN`](crate::relationship::RelationshipTarget::LINKED_SPAWN). /// This will produce "deep" / recursive clones of relationship trees that have "linked spawn". pub fn linked_cloning(&mut self, linked_cloning: bool) -> &mut Self { - self.entity_cloner.linked_cloning = linked_cloning; + self.state.linked_cloning = linked_cloning; + self + } +} + +impl<'w> EntityClonerBuilder<'w, OptOut> { + /// By default, any components denied through the filter will automatically + /// deny all of components they are required by too. + /// + /// This method allows for a scoped mode where any changes to the filter + /// will not involve these requiring components. + /// + /// If component `A` is denied in the `builder` closure here and component `B` + /// requires `A`, then `A` will be inserted with the value defined in `B`'s + /// [`Component` derive](https://docs.rs/bevy/latest/bevy/ecs/component/trait.Component.html#required-components). + /// This assumes `A` is missing yet at the target entity. + pub fn without_required_by_components(&mut self, builder: impl FnOnce(&mut Self)) -> &mut Self { + self.filter.attach_required_by_components = false; + builder(self); + self.filter.attach_required_by_components = true; self } - /// Helper function that allows a component through the filter. - fn filter_allow(&mut self, id: ComponentId) { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.insert(id); - } else { - self.entity_cloner.filter.remove(&id); + /// Sets whether components are always cloned ([`InsertMode::Replace`], the default) or only if it is missing + /// ([`InsertMode::Keep`]) at the target entity. + /// + /// This makes no difference if the target is spawned by the cloner. + pub fn insert_mode(&mut self, insert_mode: InsertMode) -> &mut Self { + self.filter.insert_mode = insert_mode; + self + } + + /// Disallows all components of the bundle from being cloned. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny(&mut self) -> &mut Self { + let bundle_id = self.world.register_bundle::().id(); + self.deny_by_bundle_id(bundle_id) + } + + /// Disallows all components of the bundle ID from being cloned. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny_by_bundle_id(&mut self, bundle_id: BundleId) -> &mut Self { + if let Some(bundle) = self.world.bundles().get(bundle_id) { + let ids = bundle.explicit_components().iter(); + for &id in ids { + self.filter.filter_deny(id, self.world); + } } - if self.attach_required_components { - if let Some(info) = self.world.components().get_info(id) { - for required_id in info.required_components().iter_ids() { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter_required.insert(required_id); - } else { - self.entity_cloner.filter_required.remove(&required_id); - } - } + self + } + + /// Extends the list of components that shouldn't be cloned. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for id in ids { + self.filter.filter_deny(id, self.world); + } + self + } + + /// Extends the list of components that shouldn't be cloned by type ids. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for type_id in ids { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter.filter_deny(id, self.world); + } + } + self + } +} + +impl<'w> EntityClonerBuilder<'w, OptIn> { + /// By default, any components allowed through the filter will automatically + /// allow all of their required components. + /// + /// This method allows for a scoped mode where any changes to the filter + /// will not involve required components. + /// + /// If component `A` is allowed in the `builder` closure here and requires + /// component `B`, then `B` will be inserted with the value defined in `A`'s + /// [`Component` derive](https://docs.rs/bevy/latest/bevy/ecs/component/trait.Component.html#required-components). + /// This assumes `B` is missing yet at the target entity. + pub fn without_required_components(&mut self, builder: impl FnOnce(&mut Self)) -> &mut Self { + self.filter.attach_required_components = false; + builder(self); + self.filter.attach_required_components = true; + self + } + + /// Adds all components of the bundle to the list of components to clone. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow(&mut self) -> &mut Self { + let bundle_id = self.world.register_bundle::().id(); + self.allow_by_bundle_id(bundle_id) + } + + /// Adds all components of the bundle to the list of components to clone if + /// the target does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_if_new(&mut self) -> &mut Self { + let bundle_id = self.world.register_bundle::().id(); + self.allow_by_bundle_id_if_new(bundle_id) + } + + /// Adds all components of the bundle ID to the list of components to clone. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_bundle_id(&mut self, bundle_id: BundleId) -> &mut Self { + if let Some(bundle) = self.world.bundles().get(bundle_id) { + let ids = bundle.explicit_components().iter(); + for &id in ids { + self.filter + .filter_allow(id, self.world, InsertMode::Replace); + } + } + self + } + + /// Adds all components of the bundle ID to the list of components to clone + /// if the target does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_bundle_id_if_new(&mut self, bundle_id: BundleId) -> &mut Self { + if let Some(bundle) = self.world.bundles().get(bundle_id) { + let ids = bundle.explicit_components().iter(); + for &id in ids { + self.filter.filter_allow(id, self.world, InsertMode::Keep); + } + } + self + } + + /// Extends the list of components to clone. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for id in ids { + self.filter + .filter_allow(id, self.world, InsertMode::Replace); + } + self + } + + /// Extends the list of components to clone if the target does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_ids_if_new(&mut self, ids: impl IntoIterator) -> &mut Self { + for id in ids { + self.filter.filter_allow(id, self.world, InsertMode::Keep); + } + self + } + + /// Extends the list of components to clone using [`TypeId`]s. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for type_id in ids { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter + .filter_allow(id, self.world, InsertMode::Replace); + } + } + self + } + + /// Extends the list of components to clone using [`TypeId`]s if the target + /// does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_type_ids_if_new(&mut self, ids: impl IntoIterator) -> &mut Self { + for type_id in ids { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter.filter_allow(id, self.world, InsertMode::Keep); + } + } + self + } +} + +/// Filters that can selectively clone components depending on its inner configuration are unified with this trait. +#[doc(hidden)] +pub trait CloneByFilter: Into { + /// The filter will call `clone_component` for every [`ComponentId`] that passes it. + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + clone_component: impl FnMut(ComponentId), + ); +} + +/// Part of the [`EntityCloner`], see there for more information. +#[doc(hidden)] +#[derive(From)] +pub enum EntityClonerFilter { + OptOut(OptOut), + OptIn(OptIn), +} + +impl Default for EntityClonerFilter { + fn default() -> Self { + Self::OptOut(Default::default()) + } +} + +impl CloneByFilter for EntityClonerFilter { + #[inline] + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + clone_component: impl FnMut(ComponentId), + ) { + match self { + Self::OptOut(filter) => { + filter.clone_components(source_archetype, target_archetype, clone_component); + } + Self::OptIn(filter) => { + filter.clone_components(source_archetype, target_archetype, clone_component); } } } +} - /// Helper function that disallows a component through the filter. - fn filter_deny(&mut self, id: ComponentId) { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.remove(&id); - } else { - self.entity_cloner.filter.insert(id); +/// Generic for [`EntityClonerBuilder`] that makes the cloner try to clone every component from the source entity +/// except for components that were explicitly denied, for example by using the +/// [`deny`](EntityClonerBuilder::deny) method. +/// +/// Required components are not considered by denied components and must be explicitly denied as well if desired. +pub struct OptOut { + /// Contains the components that should not be cloned. + deny: HashSet, + + /// Determines if a component is inserted when it is existing already. + insert_mode: InsertMode, + + /// Is `true` unless during [`EntityClonerBuilder::without_required_by_components`] which will suppress + /// components that require denied components to be denied as well, causing them to be created independent + /// from the value at the source entity if needed. + attach_required_by_components: bool, +} + +impl Default for OptOut { + fn default() -> Self { + Self { + deny: Default::default(), + insert_mode: InsertMode::Replace, + attach_required_by_components: true, } - if self.attach_required_components { - if let Some(info) = self.world.components().get_info(id) { - for required_id in info.required_components().iter_ids() { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter_required.remove(&required_id); - } else { - self.entity_cloner.filter_required.insert(required_id); + } +} + +impl CloneByFilter for OptOut { + #[inline] + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + mut clone_component: impl FnMut(ComponentId), + ) { + match self.insert_mode { + InsertMode::Replace => { + for component in source_archetype.components() { + if !self.deny.contains(&component) { + clone_component(component); + } + } + } + InsertMode::Keep => { + for component in source_archetype.components() { + if !target_archetype.contains(component) && !self.deny.contains(&component) { + clone_component(component); } } } @@ -852,30 +1067,264 @@ impl<'w> EntityClonerBuilder<'w> { } } +impl OptOut { + /// Denies a component through the filter, also deny components that require `id` if + /// [`Self::attach_required_by_components`] is true. + #[inline] + fn filter_deny(&mut self, id: ComponentId, world: &World) { + self.deny.insert(id); + if self.attach_required_by_components { + if let Some(required_by) = world.components().get_required_by(id) { + self.deny.extend(required_by.iter()); + }; + } + } +} + +/// Generic for [`EntityClonerBuilder`] that makes the cloner try to clone every component that was explicitly +/// allowed from the source entity, for example by using the [`allow`](EntityClonerBuilder::allow) method. +/// +/// Required components are also cloned when the target entity does not contain them. +pub struct OptIn { + /// Contains the components explicitly allowed to be cloned. + allow: HashMap, + + /// Lists of required components, [`Explicit`] refers to a range in it. + required_of_allow: Vec, + + /// Contains the components required by those in [`Self::allow`]. + /// Also contains the number of components in [`Self::allow`] each is required by to track + /// when to skip cloning a required component after skipping explicit components that require it. + required: HashMap, + + /// Is `true` unless during [`EntityClonerBuilder::without_required_components`] which will suppress + /// evaluating required components to clone, causing them to be created independent from the value at + /// the source entity if needed. + attach_required_components: bool, +} + +impl Default for OptIn { + fn default() -> Self { + Self { + allow: Default::default(), + required_of_allow: Default::default(), + required: Default::default(), + attach_required_components: true, + } + } +} + +impl CloneByFilter for OptIn { + #[inline] + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + mut clone_component: impl FnMut(ComponentId), + ) { + // track the amount of components left not being cloned yet to exit this method early + let mut uncloned_components = source_archetype.component_count(); + + // track if any `Required::required_by_reduced` has been reduced so they are reset + let mut reduced_any = false; + + // clone explicit components + for (&component, explicit) in self.allow.iter() { + if uncloned_components == 0 { + // exhausted all source components, reset changed `Required::required_by_reduced` + if reduced_any { + self.required + .iter_mut() + .for_each(|(_, required)| required.reset()); + } + return; + } + + let do_clone = source_archetype.contains(component) + && (explicit.insert_mode == InsertMode::Replace + || !target_archetype.contains(component)); + if do_clone { + clone_component(component); + uncloned_components -= 1; + } else if let Some(range) = explicit.required_range.clone() { + for component in self.required_of_allow[range].iter() { + // may be None if required component was also added as explicit later + if let Some(required) = self.required.get_mut(component) { + required.required_by_reduced -= 1; + reduced_any = true; + } + } + } + } + + let mut required_iter = self.required.iter_mut(); + + // clone required components + let required_components = required_iter + .by_ref() + .filter_map(|(&component, required)| { + let do_clone = required.required_by_reduced > 0 // required by a cloned component + && source_archetype.contains(component) // must exist to clone, may miss if removed + && !target_archetype.contains(component); // do not overwrite existing values + + // reset changed `Required::required_by_reduced` as this is done being checked here + required.reset(); + + do_clone.then_some(component) + }) + .take(uncloned_components); + + for required_component in required_components { + clone_component(required_component); + } + + // if the `required_components` iterator has not been exhausted yet because the source has no more + // components to clone, iterate the rest to reset changed `Required::required_by_reduced` for the + // next clone + if reduced_any { + required_iter.for_each(|(_, required)| required.reset()); + } + } +} + +impl OptIn { + /// Allows a component through the filter, also allow required components if + /// [`Self::attach_required_components`] is true. + #[inline] + fn filter_allow(&mut self, id: ComponentId, world: &World, mut insert_mode: InsertMode) { + match self.allow.entry(id) { + Entry::Vacant(explicit) => { + // explicit components should not appear in the required map + self.required.remove(&id); + + if !self.attach_required_components { + explicit.insert(Explicit { + insert_mode, + required_range: None, + }); + } else { + self.filter_allow_with_required(id, world, insert_mode); + } + } + Entry::Occupied(mut explicit) => { + let explicit = explicit.get_mut(); + + // set required component range if it was inserted with `None` earlier + if self.attach_required_components && explicit.required_range.is_none() { + if explicit.insert_mode == InsertMode::Replace { + // do not overwrite with Keep if component was allowed as Replace earlier + insert_mode = InsertMode::Replace; + } + + self.filter_allow_with_required(id, world, insert_mode); + } else if explicit.insert_mode == InsertMode::Keep { + // potentially overwrite Keep with Replace + explicit.insert_mode = insert_mode; + } + } + }; + } + + // Allow a component through the filter and include required components. + #[inline] + fn filter_allow_with_required( + &mut self, + id: ComponentId, + world: &World, + insert_mode: InsertMode, + ) { + let Some(info) = world.components().get_info(id) else { + return; + }; + + let iter = info + .required_components() + .iter_ids() + .filter(|id| !self.allow.contains_key(id)) + .inspect(|id| { + // set or increase the number of components this `id` is required by + self.required + .entry(*id) + .and_modify(|required| { + required.required_by += 1; + required.required_by_reduced += 1; + }) + .or_insert(Required { + required_by: 1, + required_by_reduced: 1, + }); + }); + + let start = self.required_of_allow.len(); + self.required_of_allow.extend(iter); + let end = self.required_of_allow.len(); + + self.allow.insert( + id, + Explicit { + insert_mode, + required_range: Some(start..end), + }, + ); + } +} + +/// Contains the components explicitly allowed to be cloned. +struct Explicit { + /// If component was added via [`allow`](EntityClonerBuilder::allow) etc, this is `Overwrite`. + /// + /// If component was added via [`allow_if_new`](EntityClonerBuilder::allow_if_new) etc, this is `Keep`. + insert_mode: InsertMode, + + /// Contains the range in [`OptIn::required_of_allow`] for this component containing its + /// required components. + /// + /// Is `None` if [`OptIn::attach_required_components`] was `false` when added. + /// It may be set to `Some` later if the component is later added explicitly again with + /// [`OptIn::attach_required_components`] being `true`. + /// + /// Range is empty if this component has no required components that are not also explicitly allowed. + required_range: Option>, +} + +struct Required { + /// Amount of explicit components this component is required by. + required_by: u32, + + /// As [`Self::required_by`] but is reduced during cloning when an explicit component is not cloned, + /// either because [`Explicit::insert_mode`] is `Keep` or the source entity does not contain it. + /// + /// If this is zero, the required component is not cloned. + /// + /// The counter is reset to `required_by` when the cloning is over in case another entity needs to be + /// cloned by the same [`EntityCloner`]. + required_by_reduced: u32, +} + +impl Required { + // Revert reductions for the next entity to clone with this EntityCloner + #[inline] + fn reset(&mut self) { + self.required_by_reduced = self.required_by; + } +} + #[cfg(test)] mod tests { - use super::ComponentCloneCtx; + use super::*; use crate::{ - component::{Component, ComponentCloneBehavior, ComponentDescriptor, StorageType}, - entity::{Entity, EntityCloner, EntityHashMap, SourceComponent}, + component::{ComponentDescriptor, StorageType}, prelude::{ChildOf, Children, Resource}, - reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld}, world::{FromWorld, World}, }; - use alloc::vec::Vec; use bevy_ptr::OwningPtr; - use bevy_reflect::Reflect; use core::marker::PhantomData; use core::{alloc::Layout, ops::Deref}; #[cfg(feature = "bevy_reflect")] mod reflect { use super::*; - use crate::{ - component::{Component, ComponentCloneBehavior}, - entity::{EntityCloner, SourceComponent}, - reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld}, - }; + use crate::reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld}; use alloc::vec; use bevy_reflect::{std_traits::ReflectDefault, FromType, Reflect, ReflectFromPtr}; @@ -898,7 +1347,7 @@ mod tests { let e = world.spawn(component.clone()).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior::(ComponentCloneBehavior::reflect()) .clone_entity(e, e_clone); @@ -983,7 +1432,7 @@ mod tests { .id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior_with_id(a_id, ComponentCloneBehavior::reflect()) .override_clone_behavior_with_id(b_id, ComponentCloneBehavior::reflect()) .override_clone_behavior_with_id(c_id, ComponentCloneBehavior::reflect()) @@ -1016,7 +1465,7 @@ mod tests { let mut registry = registry.write(); registry.register::(); registry - .get_mut(core::any::TypeId::of::()) + .get_mut(TypeId::of::()) .unwrap() .insert(>::from_type()); } @@ -1024,7 +1473,7 @@ mod tests { let e = world.spawn(A).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior::(ComponentCloneBehavior::Custom(test_handler)) .clone_entity(e, e_clone); } @@ -1053,7 +1502,7 @@ mod tests { let e = world.spawn(component.clone()).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(e, e_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(e, e_clone); assert!(world .get::(e_clone) @@ -1077,7 +1526,7 @@ mod tests { // No AppTypeRegistry let e = world.spawn((A, B(Default::default()))).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior::(ComponentCloneBehavior::reflect()) .override_clone_behavior::(ComponentCloneBehavior::reflect()) .clone_entity(e, e_clone); @@ -1091,10 +1540,50 @@ mod tests { let e = world.spawn((A, B(Default::default()))).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(e, e_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(e, e_clone); assert_eq!(world.get::(e_clone), None); assert_eq!(world.get::(e_clone), None); } + + #[test] + fn clone_with_reflect_from_world() { + #[derive(Component, Reflect, PartialEq, Eq, Debug)] + #[reflect(Component, FromWorld, from_reflect = false)] + struct SomeRef( + #[entities] Entity, + // We add an ignored field here to ensure `reflect_clone` fails and `FromWorld` is used + #[reflect(ignore)] PhantomData<()>, + ); + + #[derive(Resource)] + struct FromWorldCalled(bool); + + impl FromWorld for SomeRef { + fn from_world(world: &mut World) -> Self { + world.insert_resource(FromWorldCalled(true)); + SomeRef(Entity::PLACEHOLDER, Default::default()) + } + } + let mut world = World::new(); + let registry = AppTypeRegistry::default(); + registry.write().register::(); + world.insert_resource(registry); + + let a = world.spawn_empty().id(); + let b = world.spawn_empty().id(); + let c = world.spawn(SomeRef(a, Default::default())).id(); + let d = world.spawn_empty().id(); + let mut map = EntityHashMap::::new(); + map.insert(a, b); + map.insert(c, d); + + let cloned = EntityCloner::default().clone_entity_mapped(&mut world, c, &mut map); + assert_eq!( + *world.entity(cloned).get::().unwrap(), + SomeRef(b, Default::default()) + ); + assert!(world.resource::().0); + } } #[test] @@ -1111,7 +1600,7 @@ mod tests { let e = world.spawn(component.clone()).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(e, e_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(e, e_clone); assert!(world.get::(e_clone).is_some_and(|c| *c == component)); } @@ -1133,8 +1622,7 @@ mod tests { let e = world.spawn((component.clone(), B)).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .allow::() .clone_entity(e, e_clone); @@ -1150,9 +1638,10 @@ mod tests { } #[derive(Component, Clone)] + #[require(C)] struct B; - #[derive(Component, Clone)] + #[derive(Component, Clone, Default)] struct C; let mut world = World::default(); @@ -1162,72 +1651,8 @@ mod tests { let e = world.spawn((component.clone(), B, C)).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny::() - .clone_entity(e, e_clone); - - assert!(world.get::(e_clone).is_some_and(|c| *c == component)); - assert!(world.get::(e_clone).is_none()); - assert!(world.get::(e_clone).is_some()); - } - - #[test] - fn clone_entity_with_override_allow_filter() { - #[derive(Component, Clone, PartialEq, Eq)] - struct A { - field: usize, - } - - #[derive(Component, Clone)] - struct B; - - #[derive(Component, Clone)] - struct C; - - let mut world = World::default(); - - let component = A { field: 5 }; - - let e = world.spawn((component.clone(), B, C)).id(); - let e_clone = world.spawn_empty().id(); - - EntityCloner::build(&mut world) - .deny_all() - .allow::() - .allow::() - .allow::() - .deny::() - .clone_entity(e, e_clone); - - assert!(world.get::(e_clone).is_some_and(|c| *c == component)); - assert!(world.get::(e_clone).is_none()); - assert!(world.get::(e_clone).is_some()); - } - - #[test] - fn clone_entity_with_override_bundle() { - #[derive(Component, Clone, PartialEq, Eq)] - struct A { - field: usize, - } - - #[derive(Component, Clone)] - struct B; - - #[derive(Component, Clone)] - struct C; - - let mut world = World::default(); - - let component = A { field: 5 }; - - let e = world.spawn((component.clone(), B, C)).id(); - let e_clone = world.spawn_empty().id(); - - EntityCloner::build(&mut world) - .deny_all() - .allow::<(A, B, C)>() - .deny::<(B, C)>() + EntityCloner::build_opt_out(&mut world) + .deny::() .clone_entity(e, e_clone); assert!(world.get::(e_clone).is_some_and(|c| *c == component)); @@ -1235,6 +1660,171 @@ mod tests { assert!(world.get::(e_clone).is_none()); } + #[test] + fn clone_entity_with_deny_filter_without_required_by() { + #[derive(Component, Clone)] + #[require(B { field: 5 })] + struct A; + + #[derive(Component, Clone, PartialEq, Eq)] + struct B { + field: usize, + } + + let mut world = World::default(); + + let e = world.spawn((A, B { field: 10 })).id(); + let e_clone = world.spawn_empty().id(); + + EntityCloner::build_opt_out(&mut world) + .without_required_by_components(|builder| { + builder.deny::(); + }) + .clone_entity(e, e_clone); + + assert!(world.get::(e_clone).is_some()); + assert!(world + .get::(e_clone) + .is_some_and(|c| *c == B { field: 5 })); + } + + #[test] + fn clone_entity_with_deny_filter_if_new() { + #[derive(Component, Clone, PartialEq, Eq)] + struct A { + field: usize, + } + + #[derive(Component, Clone)] + struct B; + + #[derive(Component, Clone)] + struct C; + + let mut world = World::default(); + + let e = world.spawn((A { field: 5 }, B, C)).id(); + let e_clone = world.spawn(A { field: 8 }).id(); + + EntityCloner::build_opt_out(&mut world) + .deny::() + .insert_mode(InsertMode::Keep) + .clone_entity(e, e_clone); + + assert!(world + .get::(e_clone) + .is_some_and(|c| *c == A { field: 8 })); + assert!(world.get::(e_clone).is_none()); + assert!(world.get::(e_clone).is_some()); + } + + #[test] + fn allow_and_allow_if_new_always_allows() { + #[derive(Component, Clone, PartialEq, Debug)] + struct A(u8); + + let mut world = World::default(); + let e = world.spawn(A(1)).id(); + let e_clone1 = world.spawn(A(2)).id(); + + EntityCloner::build_opt_in(&mut world) + .allow_if_new::() + .allow::() + .clone_entity(e, e_clone1); + + assert_eq!(world.get::(e_clone1), Some(&A(1))); + + let e_clone2 = world.spawn(A(2)).id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .allow_if_new::() + .clone_entity(e, e_clone2); + + assert_eq!(world.get::(e_clone2), Some(&A(1))); + } + + #[test] + fn with_and_without_required_components_include_required() { + #[derive(Component, Clone, PartialEq, Debug)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u8); + + let mut world = World::default(); + let e = world.spawn((A, B(10))).id(); + let e_clone1 = world.spawn_empty().id(); + EntityCloner::build_opt_in(&mut world) + .without_required_components(|builder| { + builder.allow::(); + }) + .allow::() + .clone_entity(e, e_clone1); + + assert_eq!(world.get::(e_clone1), Some(&B(10))); + + let e_clone2 = world.spawn_empty().id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .without_required_components(|builder| { + builder.allow::(); + }) + .clone_entity(e, e_clone2); + + assert_eq!(world.get::(e_clone2), Some(&B(10))); + } + + #[test] + fn clone_required_becoming_explicit() { + #[derive(Component, Clone, PartialEq, Debug)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u8); + + let mut world = World::default(); + let e = world.spawn((A, B(10))).id(); + let e_clone1 = world.spawn(B(20)).id(); + EntityCloner::build_opt_in(&mut world) + .allow::() + .allow::() + .clone_entity(e, e_clone1); + + assert_eq!(world.get::(e_clone1), Some(&B(10))); + + let e_clone2 = world.spawn(B(20)).id(); + EntityCloner::build_opt_in(&mut world) + .allow::() + .allow::() + .clone_entity(e, e_clone2); + + assert_eq!(world.get::(e_clone2), Some(&B(10))); + } + + #[test] + fn required_not_cloned_because_requiring_missing() { + #[derive(Component, Clone)] + #[require(B)] + struct A; + + #[derive(Component, Clone, Default)] + struct B; + + let mut world = World::default(); + let e = world.spawn(B).id(); + let e_clone1 = world.spawn_empty().id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .clone_entity(e, e_clone1); + + assert!(world.get::(e_clone1).is_none()); + } + #[test] fn clone_entity_with_required_components() { #[derive(Component, Clone, PartialEq, Debug)] @@ -1253,8 +1843,7 @@ mod tests { let e = world.spawn(A).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .allow::() .clone_entity(e, e_clone); @@ -1281,8 +1870,7 @@ mod tests { let e = world.spawn((A, C(0))).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .without_required_components(|builder| { builder.allow::(); }) @@ -1293,6 +1881,60 @@ mod tests { assert_eq!(world.entity(e_clone).get::(), Some(&C(5))); } + #[test] + fn clone_entity_with_missing_required_components() { + #[derive(Component, Clone, PartialEq, Debug)] + #[require(B)] + struct A; + + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(C(5))] + struct B; + + #[derive(Component, Clone, PartialEq, Debug)] + struct C(u32); + + let mut world = World::default(); + + let e = world.spawn(A).remove::().id(); + let e_clone = world.spawn_empty().id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .clone_entity(e, e_clone); + + assert_eq!(world.entity(e_clone).get::(), Some(&A)); + assert_eq!(world.entity(e_clone).get::(), Some(&B)); + assert_eq!(world.entity(e_clone).get::(), Some(&C(5))); + } + + #[test] + fn skipped_required_components_counter_is_reset_on_early_return() { + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u32); + + #[derive(Component, Clone, PartialEq, Debug, Default)] + struct C; + + let mut world = World::default(); + + let e1 = world.spawn(C).id(); + let e2 = world.spawn((A, B(0))).id(); + let e_clone = world.spawn_empty().id(); + + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.allow::<(A, C)>(); + let mut cloner = builder.finish(); + cloner.clone_entity(&mut world, e1, e_clone); + cloner.clone_entity(&mut world, e2, e_clone); + + assert_eq!(world.entity(e_clone).get::(), Some(&B(0))); + } + #[test] fn clone_entity_with_dynamic_components() { const COMPONENT_SIZE: usize = 10; @@ -1333,7 +1975,7 @@ mod tests { let entity = entity.id(); let entity_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(entity, entity_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(entity, entity_clone); let ptr = world.get_by_id(entity, component_id).unwrap(); let clone_ptr = world.get_by_id(entity_clone, component_id).unwrap(); @@ -1355,7 +1997,7 @@ mod tests { let child2 = world.spawn(ChildOf(root)).id(); let clone_root = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .linked_cloning(true) .clone_entity(root, clone_root); @@ -1380,46 +2022,6 @@ mod tests { ); } - #[test] - fn clone_with_reflect_from_world() { - #[derive(Component, Reflect, PartialEq, Eq, Debug)] - #[reflect(Component, FromWorld, from_reflect = false)] - struct SomeRef( - #[entities] Entity, - // We add an ignored field here to ensure `reflect_clone` fails and `FromWorld` is used - #[reflect(ignore)] PhantomData<()>, - ); - - #[derive(Resource)] - struct FromWorldCalled(bool); - - impl FromWorld for SomeRef { - fn from_world(world: &mut World) -> Self { - world.insert_resource(FromWorldCalled(true)); - SomeRef(Entity::PLACEHOLDER, Default::default()) - } - } - let mut world = World::new(); - let registry = AppTypeRegistry::default(); - registry.write().register::(); - world.insert_resource(registry); - - let a = world.spawn_empty().id(); - let b = world.spawn_empty().id(); - let c = world.spawn(SomeRef(a, Default::default())).id(); - let d = world.spawn_empty().id(); - let mut map = EntityHashMap::::new(); - map.insert(a, b); - map.insert(c, d); - - let cloned = EntityCloner::default().clone_entity_mapped(&mut world, c, &mut map); - assert_eq!( - *world.entity(cloned).get::().unwrap(), - SomeRef(b, Default::default()) - ); - assert!(world.resource::().0); - } - #[test] fn cloning_with_required_components_preserves_existing() { #[derive(Component, Clone, PartialEq, Debug, Default)] @@ -1434,21 +2036,11 @@ mod tests { let e = world.spawn((A, B(0))).id(); let e_clone = world.spawn(B(1)).id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .allow::() .clone_entity(e, e_clone); assert_eq!(world.entity(e_clone).get::(), Some(&A)); assert_eq!(world.entity(e_clone).get::(), Some(&B(1))); - - let e_clone2 = world.spawn(B(2)).id(); - - EntityCloner::build(&mut world) - .allow_all() - .deny::() - .clone_entity(e, e_clone2); - - assert_eq!(world.entity(e_clone2).get::(), Some(&B(2))); } } diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 0e22fddbc8..700a4e517f 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -76,7 +76,7 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, change_detection::MaybeLocation, - component::Tick, + component::{CheckChangeTicks, Tick}, storage::{SparseSetIndex, TableId, TableRow}, }; use alloc::vec::Vec; @@ -718,6 +718,7 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> { } impl<'a> ExactSizeIterator for ReserveEntitiesIterator<'a> {} + impl<'a> core::iter::FusedIterator for ReserveEntitiesIterator<'a> {} // SAFETY: Newly reserved entity values are unique. @@ -1216,9 +1217,9 @@ impl Entities { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for meta in &mut self.meta { - meta.spawned_or_despawned.at.check_tick(change_tick); + meta.spawned_or_despawned.at.check_tick(check); } } diff --git a/crates/bevy_ecs/src/entity/unique_array.rs b/crates/bevy_ecs/src/entity/unique_array.rs index ce31e55448..71df33ec5f 100644 --- a/crates/bevy_ecs/src/entity/unique_array.rs +++ b/crates/bevy_ecs/src/entity/unique_array.rs @@ -154,6 +154,7 @@ impl DerefMut for UniqueEntityEquivalentArr unsafe { UniqueEntityEquivalentSlice::from_slice_unchecked_mut(&mut self.0) } } } + impl Default for UniqueEntityEquivalentArray { fn default() -> Self { Self(Default::default()) @@ -527,6 +528,7 @@ impl, U: EntityEquivalent, const N: usize> self.eq(&other.0) } } + impl, U: EntityEquivalent, const N: usize> PartialEq<&UniqueEntityEquivalentArray> for VecDeque { @@ -550,6 +552,7 @@ impl, U: EntityEquivalent, const N: usize> self.eq(&other.0) } } + impl, U: EntityEquivalent, const N: usize> PartialEq> for VecDeque { diff --git a/crates/bevy_ecs/src/error/command_handling.rs b/crates/bevy_ecs/src/error/command_handling.rs index bf2741d376..c303b76d17 100644 --- a/crates/bevy_ecs/src/error/command_handling.rs +++ b/crates/bevy_ecs/src/error/command_handling.rs @@ -1,4 +1,6 @@ -use core::{any::type_name, fmt}; +use core::fmt; + +use bevy_utils::prelude::DebugName; use crate::{ entity::Entity, @@ -31,7 +33,7 @@ where Err(err) => (error_handler)( err.into(), ErrorContext::Command { - name: type_name::().into(), + name: DebugName::type_name::(), }, ), } @@ -43,7 +45,7 @@ where Err(err) => world.default_error_handler()( err.into(), ErrorContext::Command { - name: type_name::().into(), + name: DebugName::type_name::(), }, ), } diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index c89408b250..85a5a13297 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -1,7 +1,7 @@ use core::fmt::Display; use crate::{component::Tick, error::BevyError, prelude::Resource}; -use alloc::borrow::Cow; +use bevy_utils::prelude::DebugName; use derive_more::derive::{Deref, DerefMut}; /// Context for a [`BevyError`] to aid in debugging. @@ -10,26 +10,26 @@ pub enum ErrorContext { /// The error occurred in a system. System { /// The name of the system that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the system was run. last_run: Tick, }, /// The error occurred in a run condition. RunCondition { /// The name of the run condition that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the run condition was evaluated. last_run: Tick, }, /// The error occurred in a command. Command { /// The name of the command that failed. - name: Cow<'static, str>, + name: DebugName, }, /// The error occurred in an observer. Observer { /// The name of the observer that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the observer was run. last_run: Tick, }, @@ -54,12 +54,12 @@ impl Display for ErrorContext { impl ErrorContext { /// The name of the ECS construct that failed. - pub fn name(&self) -> &str { + pub fn name(&self) -> DebugName { match self { Self::System { name, .. } | Self::Command { name, .. } | Self::Observer { name, .. } - | Self::RunCondition { name, .. } => name, + | Self::RunCondition { name, .. } => name.clone(), } } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index bb896c4f09..52839f369d 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -11,33 +11,81 @@ use core::{ marker::PhantomData, }; -/// Something that "happens" and might be read / observed by app logic. +/// Something that "happens" and can be processed by app logic. /// -/// Events can be stored in an [`Events`] resource -/// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter. +/// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), +/// causing any global [`Observer`] watching that event to run. This allows for push-based +/// event handling where observers are immediately notified of events as they happen. /// -/// Events can also be "triggered" on a [`World`], which will then cause any [`Observer`] of that trigger to run. +/// Additional event handling behavior can be enabled by implementing the [`EntityEvent`] +/// and [`BufferedEvent`] traits: +/// +/// - [`EntityEvent`]s support targeting specific entities, triggering any observers watching those targets. +/// They are useful for entity-specific event handlers and can even be propagated from one entity to another. +/// - [`BufferedEvent`]s support a pull-based event handling system where events are written using an [`EventWriter`] +/// and read later using an [`EventReader`]. This is an alternative to observers that allows efficient batch processing +/// of events at fixed points in a schedule. /// /// Events must be thread-safe. /// -/// ## Derive -/// This trait can be derived. -/// Adding `auto_propagate` sets [`Self::AUTO_PROPAGATE`] to true. -/// Adding `traversal = "X"` sets [`Self::Traversal`] to be of type "X". +/// # Usage +/// +/// The [`Event`] trait can be derived: /// /// ``` -/// use bevy_ecs::prelude::*; -/// +/// # use bevy_ecs::prelude::*; +/// # /// #[derive(Event)] -/// #[event(auto_propagate)] -/// struct MyEvent; +/// struct Speak { +/// message: String, +/// } /// ``` /// +/// An [`Observer`] can then be added to listen for this event type: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// world.add_observer(|trigger: On| { +/// println!("{}", trigger.message); +/// }); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger`](World::trigger) method: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// # world.add_observer(|trigger: On| { +/// # println!("{}", trigger.message); +/// # }); +/// # +/// # world.flush(); +/// # +/// world.trigger(Speak { +/// message: "Hello!".to_string(), +/// }); +/// ``` +/// +/// For events that additionally need entity targeting or buffering, consider also deriving +/// [`EntityEvent`] or [`BufferedEvent`], respectively. /// /// [`World`]: crate::world::World -/// [`ComponentId`]: crate::component::ComponentId /// [`Observer`]: crate::observer::Observer -/// [`Events`]: super::Events /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[diagnostic::on_unimplemented( @@ -46,18 +94,6 @@ use core::{ note = "consider annotating `{Self}` with `#[derive(Event)]`" )] pub trait Event: Send + Sync + 'static { - /// The component that describes which Entity to propagate this event to next, when [propagation] is enabled. - /// - /// [propagation]: crate::observer::Trigger::propagate - type Traversal: Traversal; - - /// When true, this event will always attempt to propagate when [triggered], without requiring a call - /// to [`Trigger::propagate`]. - /// - /// [triggered]: crate::system::Commands::trigger_targets - /// [`Trigger::propagate`]: crate::observer::Trigger::propagate - const AUTO_PROPAGATE: bool = false; - /// Generates the [`ComponentId`] for this event type. /// /// If this type has already been registered, @@ -89,6 +125,209 @@ pub trait Event: Send + Sync + 'static { } } +/// An [`Event`] that can be targeted at specific entities. +/// +/// Entity events can be triggered on a [`World`] with specific entity targets using a method +/// like [`trigger_targets`](World::trigger_targets), causing any [`Observer`] watching the event +/// for those entities to run. +/// +/// Unlike basic [`Event`]s, entity events can optionally be propagated from one entity target to another +/// based on the [`EntityEvent::Traversal`] type associated with the event. This enables use cases +/// such as bubbling events to parent entities for UI purposes. +/// +/// Entity events must be thread-safe. +/// +/// # Usage +/// +/// The [`EntityEvent`] trait can be derived. The `event` attribute can be used to further configure +/// the propagation behavior: adding `auto_propagate` sets [`EntityEvent::AUTO_PROPAGATE`] to `true`, +/// while adding `traversal = X` sets [`EntityEvent::Traversal`] to be of type `X`. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. +/// #[derive(Event, EntityEvent)] +/// #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// struct Damage { +/// amount: f32, +/// } +/// ``` +/// +/// An [`Observer`] can then be added to listen for this event type for the desired entity: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// // Spawn an enemy entity. +/// let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// +/// // Spawn some armor as a child of the enemy entity. +/// // When the armor takes damage, it will bubble the event up to the enemy, +/// // which can then handle the event with its own observer. +/// let armor_piece = world +/// .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// .observe(|trigger: On, mut query: Query<&mut Health>| { +/// // Note: `On::target` only exists because this is an `EntityEvent`. +/// let mut health = query.get_mut(trigger.target()).unwrap(); +/// health.0 -= trigger.amount; +/// }) +/// .id(); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger_targets`](World::trigger_targets) method, +/// providing the desired entity target(s): +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// # let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// # let armor_piece = world +/// # .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// # .observe(|trigger: On, mut query: Query<&mut Health>| { +/// # // Note: `On::target` only exists because this is an `EntityEvent`. +/// # let mut health = query.get_mut(trigger.target()).unwrap(); +/// # health.0 -= trigger.amount; +/// # }) +/// # .id(); +/// # +/// # world.flush(); +/// # +/// world.trigger_targets(Damage { amount: 10.0 }, armor_piece); +/// ``` +/// +/// [`World`]: crate::world::World +/// [`TriggerTargets`]: crate::observer::TriggerTargets +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `EntityEvent`", + label = "invalid `EntityEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, EntityEvent)]`" +)] +pub trait EntityEvent: Event { + /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. + /// + /// [`Entity`]: crate::entity::Entity + /// [propagation]: crate::observer::On::propagate + type Traversal: Traversal; + + /// When true, this event will always attempt to propagate when [triggered], without requiring a call + /// to [`On::propagate`]. + /// + /// [triggered]: crate::system::Commands::trigger_targets + /// [`On::propagate`]: crate::observer::On::propagate + const AUTO_PROPAGATE: bool = false; +} + +/// A buffered [`Event`] for pull-based event handling. +/// +/// Buffered events can be written with [`EventWriter`] and read using the [`EventReader`] system parameter. +/// These events are stored in the [`Events`] resource, and require periodically polling the world for new events, +/// typically in a system that runs as part of a schedule. +/// +/// While the polling imposes a small overhead, buffered events are useful for efficiently batch processing +/// a large number of events at once. This can make them more efficient than [`Event`]s used by [`Observer`]s +/// for events that happen at a high frequency or in large quantities. +/// +/// Unlike [`Event`]s triggered for observers, buffered events are evaluated at fixed points in the schedule +/// rather than immediately when they are sent. This allows for more predictable scheduling and deferring +/// event processing to a later point in time. +/// +/// Buffered events do *not* trigger observers automatically when they are written via an [`EventWriter`]. +/// However, they can still also be triggered on a [`World`] in case you want both buffered and immediate +/// event handling for the same event. +/// +/// Buffered events must be thread-safe. +/// +/// # Usage +/// +/// The [`BufferedEvent`] trait can be derived: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// #[derive(Event, BufferedEvent)] +/// struct Message(String); +/// ``` +/// +/// The event can then be written to the event buffer using an [`EventWriter`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn write_hello(mut writer: EventWriter) { +/// writer.write(Message("Hello!".to_string())); +/// } +/// ``` +/// +/// Buffered events can be efficiently read using an [`EventReader`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn read_messages(mut reader: EventReader) { +/// // Process all buffered events of type `Message`. +/// for Message(message) in reader.read() { +/// println!("{message}"); +/// } +/// } +/// ``` +/// +/// [`World`]: crate::world::World +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `BufferedEvent`", + label = "invalid `BufferedEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, BufferedEvent)]`" +)] +pub trait BufferedEvent: Event {} + /// An internal type that implements [`Component`] for a given [`Event`] type. /// /// This exists so we can easily get access to a unique [`ComponentId`] for each [`Event`] type, @@ -115,7 +354,7 @@ struct EventWrapperComponent(PhantomData); derive(Reflect), reflect(Clone, Debug, PartialEq, Hash) )] -pub struct EventId { +pub struct EventId { /// Uniquely identifies the event associated with this ID. // This value corresponds to the order in which each event was added to the world. pub id: usize, @@ -125,21 +364,21 @@ pub struct EventId { pub(super) _marker: PhantomData, } -impl Copy for EventId {} +impl Copy for EventId {} -impl Clone for EventId { +impl Clone for EventId { fn clone(&self) -> Self { *self } } -impl fmt::Display for EventId { +impl fmt::Display for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { ::fmt(self, f) } } -impl fmt::Debug for EventId { +impl fmt::Debug for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -150,27 +389,27 @@ impl fmt::Debug for EventId { } } -impl PartialEq for EventId { +impl PartialEq for EventId { fn eq(&self, other: &Self) -> bool { self.id == other.id } } -impl Eq for EventId {} +impl Eq for EventId {} -impl PartialOrd for EventId { +impl PartialOrd for EventId { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for EventId { +impl Ord for EventId { fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } -impl Hash for EventId { +impl Hash for EventId { fn hash(&self, state: &mut H) { Hash::hash(&self.id, state); } @@ -178,7 +417,7 @@ impl Hash for EventId { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub(crate) struct EventInstance { +pub(crate) struct EventInstance { pub event_id: EventId, pub event: E, } diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index 66447b7de4..7d1854149e 100644 --- a/crates/bevy_ecs/src/event/collections.rs +++ b/crates/bevy_ecs/src/event/collections.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::MaybeLocation, - event::{Event, EventCursor, EventId, EventInstance}, + event::{BufferedEvent, EventCursor, EventId, EventInstance}, resource::Resource, }; use core::{ @@ -38,10 +38,11 @@ use { /// dropped silently. /// /// # Example -/// ``` -/// use bevy_ecs::event::{Event, Events}; /// -/// #[derive(Event)] +/// ``` +/// use bevy_ecs::event::{BufferedEvent, Event, Events}; +/// +/// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize /// } @@ -91,7 +92,7 @@ use { /// [`event_update_system`]: super::event_update_system #[derive(Debug, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource, Default))] -pub struct Events { +pub struct Events { /// Holds the oldest still active events. /// Note that `a.start_event_count + a.len()` should always be equal to `events_b.start_event_count`. pub(crate) events_a: EventSequence, @@ -101,7 +102,7 @@ pub struct Events { } // Derived Default impl would incorrectly require E: Default -impl Default for Events { +impl Default for Events { fn default() -> Self { Self { events_a: Default::default(), @@ -111,7 +112,7 @@ impl Default for Events { } } -impl Events { +impl Events { /// Returns the index of the oldest event stored in the event buffer. pub fn oldest_event_count(&self) -> usize { self.events_a.start_event_count @@ -286,7 +287,7 @@ impl Events { } } -impl Extend for Events { +impl Extend for Events { #[track_caller] fn extend(&mut self, iter: I) where @@ -321,13 +322,13 @@ impl Extend for Events { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default))] -pub(crate) struct EventSequence { +pub(crate) struct EventSequence { pub(crate) events: Vec>, pub(crate) start_event_count: usize, } // Derived Default impl would incorrectly require E: Default -impl Default for EventSequence { +impl Default for EventSequence { fn default() -> Self { Self { events: Default::default(), @@ -336,7 +337,7 @@ impl Default for EventSequence { } } -impl Deref for EventSequence { +impl Deref for EventSequence { type Target = Vec>; fn deref(&self) -> &Self::Target { @@ -344,7 +345,7 @@ impl Deref for EventSequence { } } -impl DerefMut for EventSequence { +impl DerefMut for EventSequence { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.events } @@ -357,7 +358,7 @@ pub struct SendBatchIds { _marker: PhantomData, } -impl Iterator for SendBatchIds { +impl Iterator for SendBatchIds { type Item = EventId; fn next(&mut self) -> Option { @@ -377,7 +378,7 @@ impl Iterator for SendBatchIds { } } -impl ExactSizeIterator for SendBatchIds { +impl ExactSizeIterator for SendBatchIds { fn len(&self) -> usize { self.event_count.saturating_sub(self.last_count) } @@ -385,12 +386,11 @@ impl ExactSizeIterator for SendBatchIds { #[cfg(test)] mod tests { - use crate::event::Events; - use bevy_ecs_macros::Event; + use crate::event::{BufferedEvent, Event, Events}; #[test] fn iter_current_update_events_iterates_over_current_events() { - #[derive(Event, Clone)] + #[derive(Event, BufferedEvent, Clone)] struct TestEvent; let mut test_events = Events::::default(); diff --git a/crates/bevy_ecs/src/event/event_cursor.rs b/crates/bevy_ecs/src/event/event_cursor.rs index ff15ef4931..70e19a732c 100644 --- a/crates/bevy_ecs/src/event/event_cursor.rs +++ b/crates/bevy_ecs/src/event/event_cursor.rs @@ -1,5 +1,6 @@ use bevy_ecs::event::{ - Event, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, Events, + BufferedEvent, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, + Events, }; #[cfg(feature = "multi_threaded")] use bevy_ecs::event::{EventMutParIter, EventParIter}; @@ -19,9 +20,9 @@ use core::marker::PhantomData; /// /// ``` /// use bevy_ecs::prelude::*; -/// use bevy_ecs::event::{Event, Events, EventCursor}; +/// use bevy_ecs::event::{BufferedEvent, Events, EventCursor}; /// -/// #[derive(Event, Clone, Debug)] +/// #[derive(Event, BufferedEvent, Clone, Debug)] /// struct MyEvent; /// /// /// A system that both sends and receives events using a [`Local`] [`EventCursor`]. @@ -50,12 +51,12 @@ use core::marker::PhantomData; /// [`EventReader`]: super::EventReader /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventCursor { +pub struct EventCursor { pub(super) last_event_count: usize, pub(super) _marker: PhantomData, } -impl Default for EventCursor { +impl Default for EventCursor { fn default() -> Self { EventCursor { last_event_count: 0, @@ -64,7 +65,7 @@ impl Default for EventCursor { } } -impl Clone for EventCursor { +impl Clone for EventCursor { fn clone(&self) -> Self { EventCursor { last_event_count: self.last_event_count, @@ -73,7 +74,7 @@ impl Clone for EventCursor { } } -impl EventCursor { +impl EventCursor { /// See [`EventReader::read`](super::EventReader::read) pub fn read<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { self.read_with_id(events).without_id() diff --git a/crates/bevy_ecs/src/event/iterators.rs b/crates/bevy_ecs/src/event/iterators.rs index f9ee74b8b0..c90aed2a19 100644 --- a/crates/bevy_ecs/src/event/iterators.rs +++ b/crates/bevy_ecs/src/event/iterators.rs @@ -1,15 +1,15 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::Iter}; /// An iterator that yields any unread events from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIterator<'a, E: Event> { +pub struct EventIterator<'a, E: BufferedEvent> { iter: EventIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIterator<'a, E> { type Item = &'a E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -35,7 +35,7 @@ impl<'a, E: Event> Iterator for EventIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -43,13 +43,13 @@ impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { /// An iterator that yields any unread events (and their IDs) from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIteratorWithId<'a, E: Event> { +pub struct EventIteratorWithId<'a, E: BufferedEvent> { reader: &'a mut EventCursor, chain: Chain>, Iter<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -81,7 +81,7 @@ impl<'a, E: Event> EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIteratorWithId<'a, E> { type Item = (&'a E, EventId); fn next(&mut self) -> Option { match self @@ -131,16 +131,16 @@ impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[cfg(feature = "multi_threaded")] #[derive(Debug)] -pub struct EventParIter<'a, E: Event> { +pub struct EventParIter<'a, E: BufferedEvent> { reader: &'a mut EventCursor, slices: [&'a [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -149,7 +149,7 @@ pub struct EventParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventParIter<'a, E> { +impl<'a, E: BufferedEvent> EventParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -248,7 +248,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -260,7 +260,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventParIter<'a, E> { type IntoIter = EventIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 3bb422b7bb..fd624d1abf 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,8 +11,8 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{Event, EventId}; -pub use bevy_ecs_macros::Event; +pub use base::{BufferedEvent, EntityEvent, Event, EventId}; +pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event}; pub use collections::{Events, SendBatchIds}; pub use event_cursor::EventCursor; #[cfg(feature = "multi_threaded")] @@ -38,17 +38,20 @@ pub use writer::EventWriter; mod tests { use alloc::{vec, vec::Vec}; use bevy_ecs::{event::*, system::assert_is_read_only_system}; - use bevy_ecs_macros::Event; + use bevy_ecs_macros::BufferedEvent; - #[derive(Event, Copy, Clone, PartialEq, Eq, Debug)] + #[derive(Event, BufferedEvent, Copy, Clone, PartialEq, Eq, Debug)] struct TestEvent { i: usize, } - #[derive(Event, Clone, PartialEq, Debug, Default)] + #[derive(Event, BufferedEvent, Clone, PartialEq, Debug, Default)] struct EmptyTestEvent; - fn get_events(events: &Events, cursor: &mut EventCursor) -> Vec { + fn get_events( + events: &Events, + cursor: &mut EventCursor, + ) -> Vec { cursor.read(events).cloned().collect::>() } diff --git a/crates/bevy_ecs/src/event/mut_iterators.rs b/crates/bevy_ecs/src/event/mut_iterators.rs index 3cb531ce78..3fa8378f23 100644 --- a/crates/bevy_ecs/src/event/mut_iterators.rs +++ b/crates/bevy_ecs/src/event/mut_iterators.rs @@ -1,17 +1,17 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::IterMut}; /// An iterator that yields any unread events from an [`EventMutator`] or [`EventCursor`]. /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIterator<'a, E: Event> { +pub struct EventMutIterator<'a, E: BufferedEvent> { iter: EventMutIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIterator<'a, E> { type Item = &'a mut E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -37,7 +37,7 @@ impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -47,13 +47,13 @@ impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIteratorWithId<'a, E: Event> { +pub struct EventMutIteratorWithId<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, chain: Chain>, IterMut<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventMutIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -84,7 +84,7 @@ impl<'a, E: Event> EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIteratorWithId<'a, E> { type Item = (&'a mut E, EventId); fn next(&mut self) -> Option { match self @@ -134,16 +134,16 @@ impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[derive(Debug)] #[cfg(feature = "multi_threaded")] -pub struct EventMutParIter<'a, E: Event> { +pub struct EventMutParIter<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, slices: [&'a mut [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -152,7 +152,7 @@ pub struct EventMutParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> EventMutParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -251,7 +251,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -263,7 +263,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventMutParIter<'a, E> { type IntoIter = EventMutIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mutator.rs b/crates/bevy_ecs/src/event/mutator.rs index e95037af5b..a9c9459119 100644 --- a/crates/bevy_ecs/src/event/mutator.rs +++ b/crates/bevy_ecs/src/event/mutator.rs @@ -1,7 +1,7 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventMutParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, system::{Local, ResMut, SystemParam}, }; @@ -15,7 +15,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event, Debug)] +/// #[derive(Event, BufferedEvent, Debug)] /// pub struct MyEvent(pub u32); // Custom event type. /// fn my_system(mut reader: EventMutator) { /// for event in reader.read() { @@ -42,13 +42,13 @@ use bevy_ecs::{ /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventMutator<'w, 's, E: Event> { +pub struct EventMutator<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, 's, E: Event> EventMutator<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventMutator<'w, 's, E> { /// Iterates over the events this [`EventMutator`] has not seen yet. This updates the /// [`EventMutator`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -69,7 +69,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -116,7 +116,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventMutator) { diff --git a/crates/bevy_ecs/src/event/reader.rs b/crates/bevy_ecs/src/event/reader.rs index 995e2ca9e9..e15b3ea9e7 100644 --- a/crates/bevy_ecs/src/event/reader.rs +++ b/crates/bevy_ecs/src/event/reader.rs @@ -1,11 +1,11 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventIterator, EventIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventIterator, EventIteratorWithId, Events}, system::{Local, Res, SystemParam}, }; -/// Reads events of type `T` in order and tracks which events have already been read. +/// Reads [`BufferedEvent`]s of type `T` in order and tracks which events have already been read. /// /// # Concurrency /// @@ -14,13 +14,13 @@ use bevy_ecs::{ /// /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventReader<'w, 's, E: Event> { +pub struct EventReader<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: Res<'w, Events>, } -impl<'w, 's, E: Event> EventReader<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventReader<'w, 's, E> { /// Iterates over the events this [`EventReader`] has not seen yet. This updates the /// [`EventReader`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -41,7 +41,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -88,7 +88,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventReader) { diff --git a/crates/bevy_ecs/src/event/registry.rs b/crates/bevy_ecs/src/event/registry.rs index 0beb41cd25..7889de62da 100644 --- a/crates/bevy_ecs/src/event/registry.rs +++ b/crates/bevy_ecs/src/event/registry.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::{DetectChangesMut, MutUntyped}, component::{ComponentId, Tick}, - event::{Event, Events}, + event::{BufferedEvent, Events}, resource::Resource, world::World, }; @@ -45,7 +45,7 @@ impl EventRegistry { /// /// If no instance of the [`EventRegistry`] exists in the world, this will add one - otherwise it will use /// the existing instance. - pub fn register_event(world: &mut World) { + pub fn register_event(world: &mut World) { // By initializing the resource here, we can be sure that it is present, // and receive the correct, up-to-date `ComponentId` even if it was previously removed. let component_id = world.init_resource::>(); @@ -82,7 +82,7 @@ impl EventRegistry { } /// Removes an event from the world and its associated [`EventRegistry`]. - pub fn deregister_events(world: &mut World) { + pub fn deregister_events(world: &mut World) { let component_id = world.init_resource::>(); let mut registry = world.get_resource_or_init::(); registry diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index 5854ab34fb..4c38401eb4 100644 --- a/crates/bevy_ecs/src/event/writer.rs +++ b/crates/bevy_ecs/src/event/writer.rs @@ -1,9 +1,9 @@ use bevy_ecs::{ - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EventId, Events, SendBatchIds}, system::{ResMut, SystemParam}, }; -/// Sends events of type `T`. +/// Sends [`BufferedEvent`]s of type `T`. /// /// # Usage /// @@ -11,7 +11,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event)] +/// #[derive(Event, BufferedEvent)] /// pub struct MyEvent; // Custom event type. /// fn my_system(mut writer: EventWriter) { /// writer.write(MyEvent); @@ -21,8 +21,8 @@ use bevy_ecs::{ /// ``` /// # Observers /// -/// "Buffered" Events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically -/// trigger any [`Observer`]s watching for that event, as each [`Event`] has different requirements regarding _if_ it will +/// "Buffered" events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically +/// trigger any [`Observer`]s watching for that event, as each [`BufferedEvent`] has different requirements regarding _if_ it will /// be triggered, and if so, _when_ it will be triggered in the schedule. /// /// # Concurrency @@ -38,7 +38,7 @@ use bevy_ecs::{ /// /// ``` /// # use bevy_ecs::{prelude::*, event::Events}; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # pub struct MyEvent; /// fn send_untyped(mut commands: Commands) { /// // Send an event of a specific type without having to declare that @@ -59,12 +59,12 @@ use bevy_ecs::{ /// /// [`Observer`]: crate::observer::Observer #[derive(SystemParam)] -pub struct EventWriter<'w, E: Event> { - #[system_param(validation_message = "Event not initialized")] +pub struct EventWriter<'w, E: BufferedEvent> { + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, E: Event> EventWriter<'w, E> { +impl<'w, E: BufferedEvent> EventWriter<'w, E> { /// Writes an `event`, which can later be read by [`EventReader`](super::EventReader)s. /// This method returns the [ID](`EventId`) of the written `event`. /// diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index dfc32e60db..fe9bf571c9 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -22,9 +22,9 @@ use alloc::{format, string::String, vec::Vec}; use bevy_reflect::std_traits::ReflectDefault; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use bevy_utils::prelude::DebugName; use core::ops::Deref; use core::slice; -use disqualified::ShortName; use log::warn; /// Stores the parent entity of this child entity with this component. @@ -294,6 +294,12 @@ impl<'w> EntityWorldMut<'w> { self.insert_related::(index, children) } + /// Insert child at specific index. + /// See also [`insert_related`](Self::insert_related). + pub fn insert_child(&mut self, index: usize, child: Entity) -> &mut Self { + self.insert_related::(index, &[child]) + } + /// Adds the given child to this entity /// See also [`add_related`](Self::add_related). pub fn add_child(&mut self, child: Entity) -> &mut Self { @@ -305,6 +311,11 @@ impl<'w> EntityWorldMut<'w> { self.remove_related::(children) } + /// Removes the relationship between this entity and the given entity. + pub fn remove_child(&mut self, child: Entity) -> &mut Self { + self.remove_related::(&[child]) + } + /// Replaces all the related children with a new set of children. pub fn replace_children(&mut self, children: &[Entity]) -> &mut Self { self.replace_related::(children) @@ -319,7 +330,7 @@ impl<'w> EntityWorldMut<'w> { /// /// # Panics /// - /// Panics when debug assertions are enabled if an invariant is is broken and the command is executed. + /// Panics when debug assertions are enabled if an invariant is broken and the command is executed. pub fn replace_children_with_difference( &mut self, entities_to_unrelate: &[Entity], @@ -374,6 +385,12 @@ impl<'a> EntityCommands<'a> { self.insert_related::(index, children) } + /// Insert children at specific index. + /// See also [`insert_related`](Self::insert_related). + pub fn insert_child(&mut self, index: usize, child: Entity) -> &mut Self { + self.insert_related::(index, &[child]) + } + /// Adds the given child to this entity pub fn add_child(&mut self, child: Entity) -> &mut Self { self.add_related::(&[child]) @@ -384,6 +401,11 @@ impl<'a> EntityCommands<'a> { self.remove_related::(children) } + /// Removes the relationship between this entity and the given entity. + pub fn remove_child(&mut self, child: Entity) -> &mut Self { + self.remove_related::(&[child]) + } + /// Replaces the children on this entity with a new list of children. pub fn replace_children(&mut self, children: &[Entity]) -> &mut Self { self.replace_related::(children) @@ -398,7 +420,7 @@ impl<'a> EntityCommands<'a> { /// /// # Panics /// - /// Panics when debug assertions are enabled if an invariant is is broken and the command is executed. + /// Panics when debug assertions are enabled if an invariant is broken and the command is executed. pub fn replace_children_with_difference( &mut self, entities_to_unrelate: &[Entity], @@ -439,11 +461,12 @@ pub fn validate_parent_has_component( { // TODO: print name here once Name lives in bevy_ecs let name: Option = None; + let debug_name = DebugName::type_name::(); warn!( "warning[B0004]: {}{name} with the {ty_name} component has a parent without {ty_name}.\n\ This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004", caller.map(|c| format!("{c}: ")).unwrap_or_default(), - ty_name = ShortName::of::(), + ty_name = debug_name.shortname(), name = name.map_or_else( || format!("Entity {entity}"), |s| format!("The {s} entity") @@ -641,6 +664,29 @@ mod tests { ); } + #[test] + fn insert_child() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut entity_world_mut = world.spawn_empty(); + + let first_children = entity_world_mut.add_children(&[child1, child2]); + + let root = first_children.insert_child(1, child3).id(); + + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with( + root, + vec![Node::new(child1), Node::new(child3), Node::new(child2)] + ) + ); + } + // regression test for https://github.com/bevyengine/bevy/pull/19134 #[test] fn insert_children_index_bound() { @@ -698,6 +744,25 @@ mod tests { ); } + #[test] + fn remove_child() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut root = world.spawn_empty(); + root.add_children(&[child1, child2, child3]); + root.remove_child(child2); + let root = root.id(); + + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with(root, vec![Node::new(child1), Node::new(child3)]) + ); + } + #[test] fn self_parenting_invalid() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 7c10127fcc..86275cd87f 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -60,13 +60,17 @@ pub mod world; pub use bevy_ptr as ptr; #[cfg(feature = "hotpatching")] -use event::Event; +use event::{BufferedEvent, Event}; /// The ECS prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] + #[expect( + deprecated, + reason = "`Trigger` was deprecated in favor of `On`, and `OnX` lifecycle events were deprecated in favor of `X` events." + )] pub use crate::{ bundle::Bundle, change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, @@ -74,11 +78,16 @@ pub mod prelude { component::Component, entity::{ContainsEntity, Entity, EntityMapper}, error::{BevyError, Result}, - event::{Event, EventMutator, EventReader, EventWriter, Events}, + event::{ + BufferedEvent, EntityEvent, Event, EventMutator, EventReader, EventWriter, Events, + }, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, - lifecycle::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, RemovedComponents}, + lifecycle::{ + Add, Despawn, Insert, OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, Remove, + RemovedComponents, Replace, + }, name::{Name, NameOrEntity}, - observer::{Observer, Trigger}, + observer::{Observer, On, Trigger}, query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, @@ -130,7 +139,7 @@ pub mod __macro_exports { /// /// Systems should refresh their inner pointers. #[cfg(feature = "hotpatching")] -#[derive(Event, Default)] +#[derive(Event, BufferedEvent, Default)] pub struct HotPatched; #[cfg(test)] @@ -1563,9 +1572,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Attempted to access or drop non-send resource bevy_ecs::tests::NonSendA from thread" - )] + #[should_panic] fn non_send_resource_drop_from_different_thread() { let mut world = World::default(); world.insert_non_send_resource(NonSendA::default()); @@ -1630,7 +1637,7 @@ mod tests { assert_eq!(q1.iter(&world).len(), 1); assert_eq!(q2.iter(&world).len(), 1); - assert_eq!(world.entities().len(), 2); + assert_eq!(world.entity_count(), 2); world.clear_entities(); @@ -1645,7 +1652,7 @@ mod tests { "world should not contain sparse set components" ); assert_eq!( - world.entities().len(), + world.entity_count(), 0, "world should not have any entities" ); @@ -2580,7 +2587,7 @@ mod tests { } #[test] - #[should_panic = "Recursive required components detected: A → B → C → B\nhelp: If this is intentional, consider merging the components."] + #[should_panic] fn required_components_recursion_errors() { #[derive(Component, Default)] #[require(B)] @@ -2598,7 +2605,7 @@ mod tests { } #[test] - #[should_panic = "Recursive required components detected: A → A\nhelp: Remove require(A)."] + #[should_panic] fn required_components_self_errors() { #[derive(Component, Default)] #[require(A)] diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index e228b416c3..e92c6cc7f9 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -16,26 +16,26 @@ //! There are five types of lifecycle events, split into two categories. First, we have lifecycle events that are triggered //! when a component is added to an entity: //! -//! - [`OnAdd`]: Triggered when a component is added to an entity that did not already have it. -//! - [`OnInsert`]: Triggered when a component is added to an entity, regardless of whether it already had it. +//! - [`Add`]: Triggered when a component is added to an entity that did not already have it. +//! - [`Insert`]: Triggered when a component is added to an entity, regardless of whether it already had it. //! -//! When both events occur, [`OnAdd`] hooks are evaluated before [`OnInsert`]. +//! When both events occur, [`Add`] hooks are evaluated before [`Insert`]. //! //! Next, we have lifecycle events that are triggered when a component is removed from an entity: //! -//! - [`OnReplace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. -//! - [`OnRemove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. -//! - [`OnDespawn`]: Triggered for each component on an entity when it is despawned. +//! - [`Replace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. +//! - [`Remove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. +//! - [`Despawn`]: Triggered for each component on an entity when it is despawned. //! -//! [`OnReplace`] hooks are evaluated before [`OnRemove`], then finally [`OnDespawn`] hooks are evaluated. +//! [`Replace`] hooks are evaluated before [`Remove`], then finally [`Despawn`] hooks are evaluated. //! -//! [`OnAdd`] and [`OnRemove`] are counterparts: they are only triggered when a component is added or removed +//! [`Add`] and [`Remove`] are counterparts: they are only triggered when a component is added or removed //! from an entity in such a way as to cause a change in the component's presence on that entity. -//! Similarly, [`OnInsert`] and [`OnReplace`] are counterparts: they are triggered when a component is added or replaced +//! Similarly, [`Insert`] and [`Replace`] are counterparts: they are triggered when a component is added or replaced //! on an entity, regardless of whether this results in a change in the component's presence on that entity. //! //! To reliably synchronize data structures using with component lifecycle events, -//! you can combine [`OnInsert`] and [`OnReplace`] to fully capture any changes to the data. +//! you can combine [`Insert`] and [`Replace`] to fully capture any changes to the data. //! This is particularly useful in combination with immutable components, //! to avoid any lifecycle-bypassing mutations. //! @@ -47,13 +47,17 @@ //! //! Each of these lifecycle events also corresponds to a fixed [`ComponentId`], //! which are assigned during [`World`] initialization. -//! For example, [`OnAdd`] corresponds to [`ON_ADD`]. +//! For example, [`Add`] corresponds to [`ADD`]. //! This is used to skip [`TypeId`](core::any::TypeId) lookups in hot paths. use crate::{ change_detection::MaybeLocation, component::{Component, ComponentId, ComponentIdFor, Tick}, entity::Entity, - event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, + event::{ + BufferedEvent, EntityEvent, Event, EventCursor, EventId, EventIterator, + EventIteratorWithId, Events, + }, + query::FilteredAccessSet, relationship::RelationshipHookMode, storage::SparseSet, system::{Local, ReadOnlySystemParam, SystemMeta, SystemParam}, @@ -310,61 +314,86 @@ impl ComponentHooks { } } -/// [`ComponentId`] for [`OnAdd`] -pub const ON_ADD: ComponentId = ComponentId::new(0); -/// [`ComponentId`] for [`OnInsert`] -pub const ON_INSERT: ComponentId = ComponentId::new(1); -/// [`ComponentId`] for [`OnReplace`] -pub const ON_REPLACE: ComponentId = ComponentId::new(2); -/// [`ComponentId`] for [`OnRemove`] -pub const ON_REMOVE: ComponentId = ComponentId::new(3); -/// [`ComponentId`] for [`OnDespawn`] -pub const ON_DESPAWN: ComponentId = ComponentId::new(4); +/// [`ComponentId`] for [`Add`] +pub const ADD: ComponentId = ComponentId::new(0); +/// [`ComponentId`] for [`Insert`] +pub const INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`Replace`] +pub const REPLACE: ComponentId = ComponentId::new(2); +/// [`ComponentId`] for [`Remove`] +pub const REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`Despawn`] +pub const DESPAWN: ComponentId = ComponentId::new(4); /// Trigger emitted when a component is inserted onto an entity that does not already have that -/// component. Runs before `OnInsert`. +/// component. Runs before `Insert`. /// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnAdd; +#[doc(alias = "OnAdd")] +pub struct Add; /// Trigger emitted when a component is inserted, regardless of whether or not the entity already -/// had that component. Runs after `OnAdd`, if it ran. +/// had that component. Runs after `Add`, if it ran. /// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnInsert; +#[doc(alias = "OnInsert")] +pub struct Insert; /// Trigger emitted when a component is removed from an entity, regardless /// of whether or not it is later replaced. /// /// Runs before the value is replaced, so you can still access the original component data. /// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnReplace; +#[doc(alias = "OnReplace")] +pub struct Replace; /// Trigger emitted when a component is removed from an entity, and runs before the component is /// removed, so you can still access the component data. /// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnRemove; +#[doc(alias = "OnRemove")] +pub struct Remove; /// Trigger emitted for each component on an entity when it is despawned. /// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnDespawn; +#[doc(alias = "OnDespawn")] +pub struct Despawn; + +/// Deprecated in favor of [`Add`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Add`.")] +pub type OnAdd = Add; + +/// Deprecated in favor of [`Insert`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Insert`.")] +pub type OnInsert = Insert; + +/// Deprecated in favor of [`Replace`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Replace`.")] +pub type OnReplace = Replace; + +/// Deprecated in favor of [`Remove`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Remove`.")] +pub type OnRemove = Remove; + +/// Deprecated in favor of [`Despawn`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Despawn`.")] +pub type OnDespawn = Despawn; /// Wrapper around [`Entity`] for [`RemovedComponents`]. /// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, Debug, Clone, Into)] +#[derive(Event, BufferedEvent, Debug, Clone, Into)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] pub struct RemovedComponentEntity(Entity); @@ -592,7 +621,15 @@ unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { type State = (); type Item<'w, 's> = &'w RemovedComponentEvents; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index cd2e946678..317c8f5017 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -141,7 +141,7 @@ pub struct NameOrEntity { pub entity: Entity, } -impl<'a> core::fmt::Display for NameOrEntityItem<'a> { +impl<'w, 's> core::fmt::Display for NameOrEntityItem<'w, 's> { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match self.name { @@ -159,6 +159,7 @@ impl From<&str> for Name { Name::new(name.to_owned()) } } + impl From for Name { #[inline(always)] fn from(name: String) -> Self { @@ -174,12 +175,14 @@ impl AsRef for Name { &self.name } } + impl From<&Name> for String { #[inline(always)] fn from(val: &Name) -> String { val.as_str().to_owned() } } + impl From for String { #[inline(always)] fn from(val: Name) -> String { @@ -274,9 +277,9 @@ mod tests { let e2 = world.spawn(name.clone()).id(); let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); - let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} assert_eq!(d1.to_string(), "0v0"); + let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); } diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs new file mode 100644 index 0000000000..e3fa6c530a --- /dev/null +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -0,0 +1,245 @@ +//! Centralized storage for observers, allowing for efficient look-ups. +//! +//! This has multiple levels: +//! - [`World::observers`] provides access to [`Observers`], which is a central storage for all observers. +//! - [`Observers`] contains multiple distinct caches in the form of [`CachedObservers`]. +//! - Most observers are looked up by the [`ComponentId`] of the event they are observing +//! - Lifecycle observers have their own fields to save lookups. +//! - [`CachedObservers`] contains maps of [`ObserverRunner`]s, which are the actual functions that will be run when the observer is triggered. +//! - These are split by target type, in order to allow for different lookup strategies. +//! - [`CachedComponentObservers`] is one of these maps, which contains observers that are specifically targeted at a component. + +use bevy_platform::collections::HashMap; + +use crate::{ + archetype::ArchetypeFlags, + change_detection::MaybeLocation, + component::ComponentId, + entity::EntityHashMap, + observer::{ObserverRunner, ObserverTrigger}, + prelude::*, + world::DeferredWorld, +}; + +/// An internal lookup table tracking all of the observers in the world. +/// +/// Stores a cache mapping trigger ids to the registered observers. +/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, +/// saving lookups for the most common triggers. +/// +/// This can be accessed via [`World::observers`]. +#[derive(Default, Debug)] +pub struct Observers { + // Cached ECS observers to save a lookup most common triggers. + add: CachedObservers, + insert: CachedObservers, + replace: CachedObservers, + remove: CachedObservers, + despawn: CachedObservers, + // Map from trigger type to set of observers listening to that trigger + cache: HashMap, +} + +impl Observers { + pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers { + use crate::lifecycle::*; + + match event_type { + ADD => &mut self.add, + INSERT => &mut self.insert, + REPLACE => &mut self.replace, + REMOVE => &mut self.remove, + DESPAWN => &mut self.despawn, + _ => self.cache.entry(event_type).or_default(), + } + } + + /// Attempts to get the observers for the given `event_type`. + /// + /// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`], + /// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module. + pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + use crate::lifecycle::*; + + match event_type { + ADD => Some(&self.add), + INSERT => Some(&self.insert), + REPLACE => Some(&self.replace), + REMOVE => Some(&self.remove), + DESPAWN => Some(&self.despawn), + _ => self.cache.get(&event_type), + } + } + + /// This will run the observers of the given `event_type`, targeting the given `entity` and `components`. + pub(crate) fn invoke( + mut world: DeferredWorld, + event_type: ComponentId, + current_target: Option, + original_target: Option, + components: impl Iterator + Clone, + data: &mut T, + propagate: &mut bool, + caller: MaybeLocation, + ) { + // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` + let (mut world, observers) = unsafe { + let world = world.as_unsafe_world_cell(); + // SAFETY: There are no outstanding world references + world.increment_trigger_id(); + let observers = world.observers(); + let Some(observers) = observers.try_get_observers(event_type) else { + return; + }; + // SAFETY: The only outstanding reference to world is `observers` + (world.into_deferred(), observers) + }; + + let trigger_for_components = components.clone(); + + let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { + (runner)( + world.reborrow(), + ObserverTrigger { + observer, + event_type, + components: components.clone().collect(), + current_target, + original_target, + caller, + }, + data.into(), + propagate, + ); + }; + // Trigger observers listening for any kind of this trigger + observers + .global_observers + .iter() + .for_each(&mut trigger_observer); + + // Trigger entity observers listening for this kind of trigger + if let Some(target_entity) = current_target { + if let Some(map) = observers.entity_observers.get(&target_entity) { + map.iter().for_each(&mut trigger_observer); + } + } + + // Trigger observers listening to this trigger targeting a specific component + trigger_for_components.for_each(|id| { + if let Some(component_observers) = observers.component_observers.get(&id) { + component_observers + .global_observers + .iter() + .for_each(&mut trigger_observer); + + if let Some(target_entity) = current_target { + if let Some(map) = component_observers + .entity_component_observers + .get(&target_entity) + { + map.iter().for_each(&mut trigger_observer); + } + } + } + }); + } + + pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { + use crate::lifecycle::*; + + match event_type { + ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), + INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), + REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), + REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), + _ => None, + } + } + + pub(crate) fn update_archetype_flags( + &self, + component_id: ComponentId, + flags: &mut ArchetypeFlags, + ) { + if self.add.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); + } + + if self.insert.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); + } + + if self.replace.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); + } + + if self.remove.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); + } + + if self.despawn.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); + } + } +} + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event. +/// +/// This is stored inside of [`Observers`], specialized for each kind of observer. +#[derive(Default, Debug)] +pub struct CachedObservers { + // Observers listening for any time this event is fired, regardless of target + // This will also respond to events targeting specific components or entities + pub(super) global_observers: ObserverMap, + // Observers listening for this trigger fired at a specific component + pub(super) component_observers: HashMap, + // Observers listening for this trigger fired at a specific entity + pub(super) entity_observers: EntityHashMap, +} + +impl CachedObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific components or entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting components. + pub fn get_component_observers(&self) -> &HashMap { + &self.component_observers + } + + /// Returns the observers listening for this trigger targeting entities. + pub fn entity_observers(&self) -> &HashMap { + &self.component_observers + } +} + +/// Map between an observer entity and its [`ObserverRunner`] +pub type ObserverMap = EntityHashMap; + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event targeted at a specific component. +/// +/// This is stored inside of [`CachedObservers`]. +#[derive(Default, Debug)] +pub struct CachedComponentObservers { + // Observers listening to events targeting this component, but not a specific entity + pub(super) global_observers: ObserverMap, + // Observers listening to events targeting this component on a specific entity + pub(super) entity_component_observers: EntityHashMap, +} + +impl CachedComponentObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting this component on a specific entity. + pub fn entity_component_observers(&self) -> &EntityHashMap { + &self.entity_component_observers + } +} diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs new file mode 100644 index 0000000000..a9a3645121 --- /dev/null +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -0,0 +1,492 @@ +//! Information about observers that is stored on the entities themselves. +//! +//! This allows for easier cleanup, better inspection, and more flexible querying. +//! +//! Each observer is associated with an entity, defined by the [`Observer`] component. +//! The [`Observer`] component contains the system that will be run when the observer is triggered, +//! and the [`ObserverDescriptor`] which contains information about what the observer is observing. +//! +//! When we watch entities, we add the [`ObservedBy`] component to those entities, +//! which links back to the observer entity. + +use core::any::Any; + +use crate::{ + component::{ComponentCloneBehavior, ComponentId, Mutable, StorageType}, + entity::Entity, + error::{ErrorContext, ErrorHandler}, + lifecycle::{ComponentHook, HookContext}, + observer::{observer_system_runner, ObserverRunner}, + prelude::*, + system::{IntoObserverSystem, ObserverSystem}, + world::DeferredWorld, +}; +use alloc::boxed::Box; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; + +#[cfg(feature = "bevy_reflect")] +use crate::prelude::ReflectComponent; + +/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". +/// +/// Observers listen for a "trigger" of a specific [`Event`]. An event can be triggered on the [`World`] +/// by calling [`World::trigger`], or if the event is an [`EntityEvent`], it can also be triggered for specific +/// entity targets using [`World::trigger_targets`]. +/// +/// Note that [`BufferedEvent`]s sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. +/// They must be triggered at a specific point in the schedule. +/// +/// # Usage +/// +/// The simplest usage of the observer pattern looks like this: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// #[derive(Event)] +/// struct Speak { +/// message: String, +/// } +/// +/// world.add_observer(|trigger: On| { +/// println!("{}", trigger.event().message); +/// }); +/// +/// // Observers currently require a flush() to be registered. In the context of schedules, +/// // this will generally be done for you. +/// world.flush(); +/// +/// world.trigger(Speak { +/// message: "Hello!".into(), +/// }); +/// ``` +/// +/// Notice that we used [`World::add_observer`]. This is just a shorthand for spawning an [`Observer`] manually: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct Speak; +/// // These are functionally the same: +/// world.add_observer(|trigger: On| {}); +/// world.spawn(Observer::new(|trigger: On| {})); +/// ``` +/// +/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct PrintNames; +/// # #[derive(Component, Debug)] +/// # struct Name; +/// world.add_observer(|trigger: On, names: Query<&Name>| { +/// for name in &names { +/// println!("{name:?}"); +/// } +/// }); +/// ``` +/// +/// Note that [`On`] must always be the first parameter. +/// +/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct SpawnThing; +/// # #[derive(Component, Debug)] +/// # struct Thing; +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// commands.spawn(Thing); +/// }); +/// ``` +/// +/// Observers can also trigger new events: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct A; +/// # #[derive(Event)] +/// # struct B; +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// commands.trigger(B); +/// }); +/// ``` +/// +/// When the commands are flushed (including these "nested triggers") they will be +/// recursively evaluated until there are no commands left, meaning nested triggers all +/// evaluate at the same time! +/// +/// If the event is an [`EntityEvent`], it can be triggered for specific entities, +/// which will be passed to the [`Observer`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// #[derive(Event, EntityEvent)] +/// struct Explode; +/// +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// println!("Entity {} goes BOOM!", trigger.target()); +/// commands.entity(trigger.target()).despawn(); +/// }); +/// +/// world.flush(); +/// +/// world.trigger_targets(Explode, entity); +/// ``` +/// +/// You can trigger multiple entities at once: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// world.trigger_targets(Explode, [e1, e2]); +/// ``` +/// +/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component, Debug)] +/// # struct Name(String); +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { +/// println!("Boom!"); +/// commands.entity(trigger.target()).despawn(); +/// }); +/// +/// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { +/// println!("The explosion fizzles! This entity is immune!"); +/// }); +/// ``` +/// +/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. +/// This protects against observer "garbage" building up over time. +/// +/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) +/// just shorthand for spawning an [`Observer`] directly: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// let mut observer = Observer::new(|trigger: On| {}); +/// observer.watch_entity(entity); +/// world.spawn(observer); +/// ``` +/// +/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! +/// +/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. +/// serves as the "source of truth" of the observer. +/// +/// [`SystemParam`]: crate::system::SystemParam +pub struct Observer { + hook_on_add: ComponentHook, + pub(crate) error_handler: Option, + pub(crate) system: Box, + pub(crate) descriptor: ObserverDescriptor, + pub(crate) last_trigger_id: u32, + pub(crate) despawned_watched_entities: u32, + pub(crate) runner: ObserverRunner, +} + +impl Observer { + /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered + /// for _any_ entity (or no entity). + /// + /// # Panics + /// + /// Panics if the given system is an exclusive system. + pub fn new>(system: I) -> Self { + let system = Box::new(IntoObserverSystem::into_system(system)); + assert!( + !system.is_exclusive(), + concat!( + "Exclusive system `{}` may not be used as observer.\n", + "Instead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." + ), + system.name() + ); + Self { + system, + descriptor: Default::default(), + hook_on_add: hook_on_add::, + error_handler: None, + runner: observer_system_runner::, + despawned_watched_entities: 0, + last_trigger_id: 0, + } + } + + /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer + pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { + Self { + system: Box::new(IntoSystem::into_system(|| {})), + descriptor: Default::default(), + hook_on_add: |mut world, hook_context| { + let default_error_handler = world.default_error_handler(); + world.commands().queue(move |world: &mut World| { + let entity = hook_context.entity; + if let Some(mut observe) = world.get_mut::(entity) { + if observe.descriptor.events.is_empty() { + return; + } + if observe.error_handler.is_none() { + observe.error_handler = Some(default_error_handler); + } + world.register_observer(entity); + } + }); + }, + error_handler: None, + runner, + despawned_watched_entities: 0, + last_trigger_id: 0, + } + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + pub fn with_entity(mut self, entity: Entity) -> Self { + self.descriptor.entities.push(entity); + self + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + /// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects. + pub fn watch_entity(&mut self, entity: Entity) { + self.descriptor.entities.push(entity); + } + + /// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// with the given component target. + pub fn with_component(mut self, component: ComponentId) -> Self { + self.descriptor.components.push(component); + self + } + + /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] + /// is triggered. + /// # Safety + /// The type of the `event` [`ComponentId`] _must_ match the actual value + /// of the event passed into the observer system. + pub unsafe fn with_event(mut self, event: ComponentId) -> Self { + self.descriptor.events.push(event); + self + } + + /// Set the error handler to use for this observer. + /// + /// See the [`error` module-level documentation](crate::error) for more information. + pub fn with_error_handler(mut self, error_handler: fn(BevyError, ErrorContext)) -> Self { + self.error_handler = Some(error_handler); + self + } + + /// Returns the [`ObserverDescriptor`] for this [`Observer`]. + pub fn descriptor(&self) -> &ObserverDescriptor { + &self.descriptor + } + + /// Returns the name of the [`Observer`]'s system . + pub fn system_name(&self) -> DebugName { + self.system.system_name() + } +} + +impl Component for Observer { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + type Mutability = Mutable; + fn on_add() -> Option { + Some(|world, context| { + let Some(observe) = world.get::(context.entity) else { + return; + }; + let hook = observe.hook_on_add; + hook(world, context); + }) + } + fn on_remove() -> Option { + Some(|mut world, HookContext { entity, .. }| { + let descriptor = core::mem::take( + &mut world + .entity_mut(entity) + .get_mut::() + .unwrap() + .as_mut() + .descriptor, + ); + world.commands().queue(move |world: &mut World| { + world.unregister_observer(entity, descriptor); + }); + }) + } +} + +/// Store information about what an [`Observer`] observes. +/// +/// This information is stored inside of the [`Observer`] component, +#[derive(Default, Clone)] +pub struct ObserverDescriptor { + /// The events the observer is watching. + pub(super) events: Vec, + + /// The components the observer is watching. + pub(super) components: Vec, + + /// The entities the observer is watching. + pub(super) entities: Vec, +} + +impl ObserverDescriptor { + /// Add the given `events` to the descriptor. + /// # Safety + /// The type of each [`ComponentId`] in `events` _must_ match the actual value + /// of the event passed into the observer. + pub unsafe fn with_events(mut self, events: Vec) -> Self { + self.events = events; + self + } + + /// Add the given `components` to the descriptor. + pub fn with_components(mut self, components: Vec) -> Self { + self.components = components; + self + } + + /// Add the given `entities` to the descriptor. + pub fn with_entities(mut self, entities: Vec) -> Self { + self.entities = entities; + self + } + + /// Returns the `events` that the observer is watching. + pub fn events(&self) -> &[ComponentId] { + &self.events + } + + /// Returns the `components` that the observer is watching. + pub fn components(&self) -> &[ComponentId] { + &self.components + } + + /// Returns the `entities` that the observer is watching. + pub fn entities(&self) -> &[Entity] { + &self.entities + } +} + +/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). +/// +/// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters +/// erased. +/// +/// The type parameters of this function _must_ match those used to create the [`Observer`]. +/// As such, it is recommended to only use this function within the [`Observer::new`] method to +/// ensure type parameters match. +fn hook_on_add>( + mut world: DeferredWorld<'_>, + HookContext { entity, .. }: HookContext, +) { + world.commands().queue(move |world: &mut World| { + let event_id = E::register_component_id(world); + let mut components = alloc::vec![]; + B::component_ids(&mut world.components_registrator(), &mut |id| { + components.push(id); + }); + if let Some(mut observer) = world.get_mut::(entity) { + observer.descriptor.events.push(event_id); + observer.descriptor.components.extend(components); + + let system: &mut dyn Any = observer.system.as_mut(); + let system: *mut dyn ObserverSystem = system.downcast_mut::().unwrap(); + // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias + unsafe { + (*system).initialize(world); + } + world.register_observer(entity); + } + }); +} + +/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. +#[derive(Default, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Component, Debug))] +pub struct ObservedBy(pub(crate) Vec); + +impl ObservedBy { + /// Provides a read-only reference to the list of entities observing this entity. + pub fn get(&self) -> &[Entity] { + &self.0 + } +} + +impl Component for ObservedBy { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + type Mutability = Mutable; + + fn on_remove() -> Option { + Some(|mut world, HookContext { entity, .. }| { + let observed_by = { + let mut component = world.get_mut::(entity).unwrap(); + core::mem::take(&mut component.0) + }; + for e in observed_by { + let (total_entities, despawned_watched_entities) = { + let Ok(mut entity_mut) = world.get_entity_mut(e) else { + continue; + }; + let Some(mut state) = entity_mut.get_mut::() else { + continue; + }; + state.despawned_watched_entities += 1; + ( + state.descriptor.entities.len(), + state.despawned_watched_entities as usize, + ) + }; + + // Despawn Observer if it has no more active sources. + if total_entities == despawned_watched_entities { + world.commands().entity(e).despawn(); + } + } + }) + } + + fn clone_behavior() -> ComponentCloneBehavior { + ComponentCloneBehavior::Ignore + } +} + +pub(crate) trait AnyNamedSystem: Any + Send + Sync + 'static { + fn system_name(&self) -> DebugName; +} + +impl AnyNamedSystem for T { + fn system_name(&self) -> DebugName { + self.name() + } +} diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_cloning.rs similarity index 54% rename from crates/bevy_ecs/src/observer/entity_observer.rs rename to crates/bevy_ecs/src/observer/entity_cloning.rs index bd45072a5a..7c7a4f69e9 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_cloning.rs @@ -1,56 +1,17 @@ +//! Logic to track observers when cloning entities. + use crate::{ - component::{Component, ComponentCloneBehavior, Mutable, StorageType}, - entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, - lifecycle::{ComponentHook, HookContext}, + component::ComponentCloneBehavior, + entity::{ + CloneByFilter, ComponentCloneCtx, EntityClonerBuilder, EntityMapper, SourceComponent, + }, + observer::ObservedBy, world::World, }; -use alloc::vec::Vec; use super::Observer; -/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. -#[derive(Default)] -pub struct ObservedBy(pub(crate) Vec); - -impl Component for ObservedBy { - const STORAGE_TYPE: StorageType = StorageType::SparseSet; - type Mutability = Mutable; - - fn on_remove() -> Option { - Some(|mut world, HookContext { entity, .. }| { - let observed_by = { - let mut component = world.get_mut::(entity).unwrap(); - core::mem::take(&mut component.0) - }; - for e in observed_by { - let (total_entities, despawned_watched_entities) = { - let Ok(mut entity_mut) = world.get_entity_mut(e) else { - continue; - }; - let Some(mut state) = entity_mut.get_mut::() else { - continue; - }; - state.despawned_watched_entities += 1; - ( - state.descriptor.entities.len(), - state.despawned_watched_entities as usize, - ) - }; - - // Despawn Observer if it has no more active sources. - if total_entities == despawned_watched_entities { - world.commands().entity(e).despawn(); - } - } - }) - } - - fn clone_behavior() -> ComponentCloneBehavior { - ComponentCloneBehavior::Ignore - } -} - -impl EntityClonerBuilder<'_> { +impl EntityClonerBuilder<'_, Filter> { /// Sets the option to automatically add cloned entities to the observers targeting source entity. pub fn add_observers(&mut self, add_observers: bool) -> &mut Self { if add_observers { @@ -85,7 +46,7 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo let event_types = observer_state.descriptor.events.clone(); let components = observer_state.descriptor.components.clone(); for event_type in event_types { - let observers = world.observers.get_observers(event_type); + let observers = world.observers.get_observers_mut(event_type); if components.is_empty() { if let Some(map) = observers.entity_observers.get(&source).cloned() { observers.entity_observers.insert(target, map); @@ -96,8 +57,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo else { continue; }; - if let Some(map) = observers.entity_map.get(&source).cloned() { - observers.entity_map.insert(target, map); + if let Some(map) = + observers.entity_component_observers.get(&source).cloned() + { + observers.entity_component_observers.insert(target, map); } } } @@ -109,14 +72,18 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo #[cfg(test)] mod tests { use crate::{ - entity::EntityCloner, event::Event, observer::Trigger, resource::Resource, system::ResMut, + entity::EntityCloner, + event::{EntityEvent, Event}, + observer::On, + resource::Resource, + system::ResMut, world::World, }; #[derive(Resource, Default)] struct Num(usize); - #[derive(Event)] + #[derive(Event, EntityEvent)] struct E; #[test] @@ -126,14 +93,14 @@ mod tests { let e = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); world.flush(); world.trigger_targets(E, e); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .add_observers(true) .clone_entity(e, e_clone); diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 6fcfa1621c..e9036eee74 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1,548 +1,160 @@ -//! Types for creating and storing [`Observer`]s +//! Observers are a push-based tool for responding to [`Event`]s. +//! +//! ## Observer targeting +//! +//! Observers can be "global", listening for events that are both targeted at and not targeted at any specific entity, +//! or they can be "entity-specific", listening for events that are targeted at specific entities. +//! +//! They can also be further refined by listening to events targeted at specific components +//! (instead of using a generic event type), as is done with the [`Add`] family of lifecycle events. +//! +//! When entities are observed, they will receive an [`ObservedBy`] component, +//! which will be updated to track the observers that are currently observing them. +//! +//! Currently, [observers cannot be retargeted after spawning](https://github.com/bevyengine/bevy/issues/19587): +//! despawn and respawn an observer as a workaround. +//! +//! ## Writing observers +//! +//! Observers are systems which implement [`IntoObserverSystem`] that listen for [`Event`]s matching their +//! type and target(s). +//! To write observer systems, use [`On`] as the first parameter of your system. +//! This parameter provides access to the specific event that triggered the observer, +//! as well as the entity that the event was targeted at, if any. +//! +//! Observers can request other data from the world, such as via a [`Query`] or [`Res`]. +//! Commonly, you might want to verify that the entity that the observable event is targeting +//! has a specific component, or meets some other condition. [`Query::get`] or [`Query::contains`] +//! on the [`On::target`] entity is a good way to do this. +//! +//! [`Commands`] can also be used inside of observers. +//! This can be particularly useful for triggering other observers! +//! +//! ## Spawning observers +//! +//! Observers can be spawned via [`World::add_observer`], or the equivalent app method. +//! This will cause an entity with the [`Observer`] component to be created, +//! which will then run the observer system whenever the event it is watching is triggered. +//! +//! You can control the targets that an observer is watching by calling [`Observer::watch_entity`] +//! once the entity is spawned, or by manually spawning an entity with the [`Observer`] component +//! configured with the desired targets. +//! +//! Observers are fundamentally defined as "entities which have the [`Observer`] component" +//! allowing you to add it manually to existing entities. +//! At first, this seems convenient, but only one observer can be added to an entity at a time, +//! regardless of the event it responds to: like always, components are unique. +//! +//! Instead, a better way to achieve a similar aim is to +//! use the [`EntityWorldMut::observe`] / [`EntityCommands::observe`] method, +//! which spawns a new observer, and configures it to watch the entity it is called on. +//! Unfortunately, observers defined in this way +//! [currently cannot be spawned as part of bundles](https://github.com/bevyengine/bevy/issues/14204). +//! +//! ## Triggering observers +//! +//! Observers are most commonly triggered by [`Commands`], +//! via [`Commands::trigger`] (for untargeted [`Event`]s) or [`Commands::trigger_targets`] (for targeted [`EntityEvent`]s). +//! Like usual, equivalent methods are available on [`World`], allowing you to reduce overhead when working with exclusive world access. +//! +//! If your observer is configured to watch for a specific component or set of components instead, +//! you can pass in [`ComponentId`]s into [`Commands::trigger_targets`] by using the [`TriggerTargets`] trait. +//! As discussed in the [`On`] documentation, this use case is rare, and is currently only used +//! for [lifecycle](crate::lifecycle) events, which are automatically emitted. +//! +//! ## Observer bubbling +//! +//! When using an [`EntityEvent`] targeted at an entity, the event can optionally be propagated to other targets, +//! typically up to parents in an entity hierarchy. +//! +//! This behavior is controlled via [`EntityEvent::Traversal`] and [`EntityEvent::AUTO_PROPAGATE`], +//! with the details of the propagation path specified by the [`Traversal`](crate::traversal::Traversal) trait. +//! +//! When auto-propagation is enabled, propagation must be manually stopped to prevent the event from +//! continuing to other targets. This can be done using the [`On::propagate`] method inside of your observer. +//! +//! ## Observer timing +//! +//! Observers are triggered via [`Commands`], which imply that they are evaluated at the next sync point in the ECS schedule. +//! Accordingly, they have full access to the world, and are evaluated sequentially, in the order that the commands were sent. +//! +//! To control the relative ordering of observers sent from different systems, +//! order the systems in the schedule relative to each other. +//! +//! Currently, Bevy does not provide [a way to specify the ordering of observers](https://github.com/bevyengine/bevy/issues/14890) +//! listening to the same event relative to each other. +//! +//! Commands sent by observers are [currently not immediately applied](https://github.com/bevyengine/bevy/issues/19569). +//! Instead, all queued observers will run, and then all of the commands from those observers will be applied. +//! Careful use of [`Schedule::apply_deferred`] may help as a workaround. +//! +//! ## Lifecycle events and observers +//! +//! It is important to note that observers, just like [hooks](crate::lifecycle::ComponentHooks), +//! can listen to and respond to [lifecycle](crate::lifecycle) events. +//! Unlike hooks, observers are not treated as an "innate" part of component behavior: +//! they can be added or removed at runtime, and multiple observers +//! can be registered for the same lifecycle event for the same component. +//! +//! The ordering of hooks versus observers differs based on the lifecycle event in question: +//! +//! - when adding components, hooks are evaluated first, then observers +//! - when removing components, observers are evaluated first, then hooks +//! +//! This allows hooks to act as constructors and destructors for components, +//! as they always have the first and final say in the component's lifecycle. +//! +//! ## Cleaning up observers +//! +//! Currently, observer entities are never cleaned up, even if their target entity(s) are despawned. +//! This won't cause any runtime overhead, but is a waste of memory and can result in memory leaks. +//! +//! If you run into this problem, you could manually scan the world for observer entities and despawn them, +//! by checking if the entity in [`Observer::descriptor`] still exists. +//! +//! ## Observers vs buffered events +//! +//! By contrast, [`EventReader`] and [`EventWriter`] ("buffered events"), are pull-based. +//! They require periodically polling the world to check for new events, typically in a system that runs as part of a schedule. +//! +//! This imposes a small overhead, making observers a better choice for extremely rare events, +//! but buffered events can be more efficient for events that are expected to occur multiple times per frame, +//! as it allows for batch processing of events. +//! +//! The difference in timing is also an important consideration: +//! buffered events are evaluated at fixed points during schedules, +//! while observers are evaluated as soon as possible after the event is triggered. +//! +//! This provides more control over the timing of buffered event evaluation, +//! but allows for a more ad hoc approach with observers, +//! and enables indefinite chaining of observers triggering other observers (for both better and worse!). -mod entity_observer; +mod centralized_storage; +mod distributed_storage; +mod entity_cloning; mod runner; +mod system_param; +mod trigger_targets; -pub use entity_observer::ObservedBy; +pub use centralized_storage::*; +pub use distributed_storage::*; pub use runner::*; -use variadics_please::all_tuples; +pub use system_param::*; +pub use trigger_targets::*; use crate::{ - archetype::ArchetypeFlags, change_detection::MaybeLocation, component::ComponentId, - entity::EntityHashMap, prelude::*, system::IntoObserverSystem, world::{DeferredWorld, *}, }; -use alloc::vec::Vec; -use bevy_platform::collections::HashMap; -use bevy_ptr::Ptr; -use core::{ - fmt::Debug, - marker::PhantomData, - ops::{Deref, DerefMut}, -}; -use smallvec::SmallVec; - -/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the -/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also -/// contains event propagation information. See [`Trigger::propagate`] for more information. -pub struct Trigger<'w, E, B: Bundle = ()> { - event: &'w mut E, - propagate: &'w mut bool, - trigger: ObserverTrigger, - _marker: PhantomData, -} - -impl<'w, E, B: Bundle> Trigger<'w, E, B> { - /// Creates a new trigger for the given event and observer information. - pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { - Self { - event, - propagate, - trigger, - _marker: PhantomData, - } - } - - /// Returns the event type of this trigger. - pub fn event_type(&self) -> ComponentId { - self.trigger.event_type - } - - /// Returns a reference to the triggered event. - pub fn event(&self) -> &E { - self.event - } - - /// Returns a mutable reference to the triggered event. - pub fn event_mut(&mut self) -> &mut E { - self.event - } - - /// Returns a pointer to the triggered event. - pub fn event_ptr(&self) -> Ptr { - Ptr::from(&self.event) - } - - /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. It may - /// be [`None`] if the trigger is not for a particular entity. - /// - /// Observable events can target specific entities. When those events fire, they will trigger - /// any observers on the targeted entities. In this case, the `target()` and `observer()` are - /// the same, because the observer that was triggered is attached to the entity that was - /// targeted by the event. - /// - /// However, it is also possible for those events to bubble up the entity hierarchy and trigger - /// observers on *different* entities, or trigger a global observer. In these cases, the - /// observing entity is *different* from the entity being targeted by the event. - /// - /// This is an important distinction: the entity reacting to an event is not always the same as - /// the entity triggered by the event. - pub fn target(&self) -> Option { - self.trigger.target - } - - /// Returns the components that triggered the observer, out of the - /// components defined in `B`. Does not necessarily include all of them as - /// `B` acts like an `OR` filter rather than an `AND` filter. - pub fn components(&self) -> &[ComponentId] { - &self.trigger.components - } - - /// Returns the [`Entity`] that observed the triggered event. - /// This allows you to despawn the observer, ceasing observation. - /// - /// # Examples - /// - /// ```rust - /// # use bevy_ecs::prelude::{Commands, Trigger}; - /// # - /// # struct MyEvent { - /// # done: bool, - /// # } - /// # - /// /// Handle `MyEvent` and if it is done, stop observation. - /// fn my_observer(trigger: Trigger, mut commands: Commands) { - /// if trigger.event().done { - /// commands.entity(trigger.observer()).despawn(); - /// return; - /// } - /// - /// // ... - /// } - /// ``` - pub fn observer(&self) -> Entity { - self.trigger.observer - } - - /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. - /// - /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events - /// use `()` which ends the path immediately and prevents propagation. - /// - /// To enable propagation, you must: - /// + Set [`Event::Traversal`] to the component you want to propagate along. - /// + Either call `propagate(true)` in the first observer or set [`Event::AUTO_PROPAGATE`] to `true`. - /// - /// You can prevent an event from propagating further using `propagate(false)`. - /// - /// [`Traversal`]: crate::traversal::Traversal - pub fn propagate(&mut self, should_propagate: bool) { - *self.propagate = should_propagate; - } - - /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. - /// - /// [`propagate`]: Trigger::propagate - pub fn get_propagate(&self) -> bool { - *self.propagate - } - - /// Returns the source code location that triggered this observer. - pub fn caller(&self) -> MaybeLocation { - self.trigger.caller - } -} - -impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Trigger") - .field("event", &self.event) - .field("propagate", &self.propagate) - .field("trigger", &self.trigger) - .field("_marker", &self._marker) - .finish() - } -} - -impl<'w, E, B: Bundle> Deref for Trigger<'w, E, B> { - type Target = E; - - fn deref(&self) -> &Self::Target { - self.event - } -} - -impl<'w, E, B: Bundle> DerefMut for Trigger<'w, E, B> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.event - } -} - -/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`]. -/// -/// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination -/// will run. -pub trait TriggerTargets { - /// The components the trigger should target. - fn components(&self) -> impl Iterator + Clone + '_; - - /// The entities the trigger should target. - fn entities(&self) -> impl Iterator + Clone + '_; -} - -impl TriggerTargets for &T { - fn components(&self) -> impl Iterator + Clone + '_ { - (**self).components() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - (**self).entities() - } -} - -impl TriggerTargets for Entity { - fn components(&self) -> impl Iterator + Clone + '_ { - [].into_iter() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) - } -} - -impl TriggerTargets for ComponentId { - fn components(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - [].into_iter() - } -} - -impl TriggerTargets for Vec { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -impl TriggerTargets for [T; N] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -impl TriggerTargets for [T] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -macro_rules! impl_trigger_targets_tuples { - ($(#[$meta:meta])* $($trigger_targets: ident),*) => { - #[expect(clippy::allow_attributes, reason = "can't guarantee violation of non_snake_case")] - #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] - $(#[$meta])* - impl<$($trigger_targets: TriggerTargets),*> TriggerTargets for ($($trigger_targets,)*) - { - fn components(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.components()); - )* - iter - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.entities()); - )* - iter - } - } - } -} - -all_tuples!( - #[doc(fake_variadic)] - impl_trigger_targets_tuples, - 0, - 15, - T -); - -/// A description of what an [`Observer`] observes. -#[derive(Default, Clone)] -pub struct ObserverDescriptor { - /// The events the observer is watching. - events: Vec, - - /// The components the observer is watching. - components: Vec, - - /// The entities the observer is watching. - entities: Vec, -} - -impl ObserverDescriptor { - /// Add the given `events` to the descriptor. - /// # Safety - /// The type of each [`ComponentId`] in `events` _must_ match the actual value - /// of the event passed into the observer. - pub unsafe fn with_events(mut self, events: Vec) -> Self { - self.events = events; - self - } - - /// Add the given `components` to the descriptor. - pub fn with_components(mut self, components: Vec) -> Self { - self.components = components; - self - } - - /// Add the given `entities` to the descriptor. - pub fn with_entities(mut self, entities: Vec) -> Self { - self.entities = entities; - self - } - - /// Returns the `events` that the observer is watching. - pub fn events(&self) -> &[ComponentId] { - &self.events - } - - /// Returns the `components` that the observer is watching. - pub fn components(&self) -> &[ComponentId] { - &self.components - } - - /// Returns the `entities` that the observer is watching. - pub fn entities(&self) -> &[Entity] { - &self.entities - } -} - -/// Event trigger metadata for a given [`Observer`], -#[derive(Debug)] -pub struct ObserverTrigger { - /// The [`Entity`] of the observer handling the trigger. - pub observer: Entity, - /// The [`Event`] the trigger targeted. - pub event_type: ComponentId, - /// The [`ComponentId`]s the trigger targeted. - components: SmallVec<[ComponentId; 2]>, - /// The entity the trigger targeted. - pub target: Option, - /// The location of the source code that triggered the observer. - pub caller: MaybeLocation, -} - -impl ObserverTrigger { - /// Returns the components that the trigger targeted. - pub fn components(&self) -> &[ComponentId] { - &self.components - } -} - -// Map between an observer entity and its runner -type ObserverMap = EntityHashMap; - -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component. -#[derive(Default, Debug)] -pub struct CachedComponentObservers { - // Observers listening to triggers targeting this component - map: ObserverMap, - // Observers listening to triggers targeting this component on a specific entity - entity_map: EntityHashMap, -} - -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger. -#[derive(Default, Debug)] -pub struct CachedObservers { - // Observers listening for any time this trigger is fired - map: ObserverMap, - // Observers listening for this trigger fired at a specific component - component_observers: HashMap, - // Observers listening for this trigger fired at a specific entity - entity_observers: EntityHashMap, -} - -/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers. -#[derive(Default, Debug)] -pub struct Observers { - // Cached ECS observers to save a lookup most common triggers. - on_add: CachedObservers, - on_insert: CachedObservers, - on_replace: CachedObservers, - on_remove: CachedObservers, - on_despawn: CachedObservers, - // Map from trigger type to set of observers - cache: HashMap, -} - -impl Observers { - pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { - use crate::lifecycle::*; - - match event_type { - ON_ADD => &mut self.on_add, - ON_INSERT => &mut self.on_insert, - ON_REPLACE => &mut self.on_replace, - ON_REMOVE => &mut self.on_remove, - ON_DESPAWN => &mut self.on_despawn, - _ => self.cache.entry(event_type).or_default(), - } - } - - pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { - use crate::lifecycle::*; - - match event_type { - ON_ADD => Some(&self.on_add), - ON_INSERT => Some(&self.on_insert), - ON_REPLACE => Some(&self.on_replace), - ON_REMOVE => Some(&self.on_remove), - ON_DESPAWN => Some(&self.on_despawn), - _ => self.cache.get(&event_type), - } - } - - /// This will run the observers of the given `event_type`, targeting the given `entity` and `components`. - pub(crate) fn invoke( - mut world: DeferredWorld, - event_type: ComponentId, - target: Option, - components: impl Iterator + Clone, - data: &mut T, - propagate: &mut bool, - caller: MaybeLocation, - ) { - // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` - let (mut world, observers) = unsafe { - let world = world.as_unsafe_world_cell(); - // SAFETY: There are no outstanding world references - world.increment_trigger_id(); - let observers = world.observers(); - let Some(observers) = observers.try_get_observers(event_type) else { - return; - }; - // SAFETY: The only outstanding reference to world is `observers` - (world.into_deferred(), observers) - }; - - let trigger_for_components = components.clone(); - - let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { - (runner)( - world.reborrow(), - ObserverTrigger { - observer, - event_type, - components: components.clone().collect(), - target, - caller, - }, - data.into(), - propagate, - ); - }; - // Trigger observers listening for any kind of this trigger - observers.map.iter().for_each(&mut trigger_observer); - - // Trigger entity observers listening for this kind of trigger - if let Some(target_entity) = target { - if let Some(map) = observers.entity_observers.get(&target_entity) { - map.iter().for_each(&mut trigger_observer); - } - } - - // Trigger observers listening to this trigger targeting a specific component - trigger_for_components.for_each(|id| { - if let Some(component_observers) = observers.component_observers.get(&id) { - component_observers - .map - .iter() - .for_each(&mut trigger_observer); - - if let Some(target_entity) = target { - if let Some(map) = component_observers.entity_map.get(&target_entity) { - map.iter().for_each(&mut trigger_observer); - } - } - } - }); - } - - pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { - use crate::lifecycle::*; - - match event_type { - ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), - ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), - ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), - ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), - ON_DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), - _ => None, - } - } - - pub(crate) fn update_archetype_flags( - &self, - component_id: ComponentId, - flags: &mut ArchetypeFlags, - ) { - if self.on_add.component_observers.contains_key(&component_id) { - flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); - } - - if self - .on_insert - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); - } - - if self - .on_replace - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); - } - - if self - .on_remove - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); - } - - if self - .on_despawn - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); - } - } -} impl World { /// Spawns a "global" [`Observer`] which will watch for the given event. /// Returns its [`Entity`] as a [`EntityWorldMut`]. /// - /// `system` can be any system whose first parameter is a [`Trigger`]. + /// `system` can be any system whose first parameter is [`On`]. /// /// **Calling [`observe`](EntityWorldMut::observe) on the returned /// [`EntityWorldMut`] will observe the observer itself, which you very @@ -556,10 +168,10 @@ impl World { /// struct A; /// /// # let mut world = World::new(); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); /// ``` @@ -588,7 +200,7 @@ impl World { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { - self.trigger_targets_dynamic_ref_with_caller(event_id, &mut event, (), caller); + self.trigger_dynamic_ref_with_caller(event_id, &mut event, caller); } } @@ -600,20 +212,41 @@ impl World { pub fn trigger_ref(&mut self, event: &mut E) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` - unsafe { self.trigger_targets_dynamic_ref(event_id, event, ()) }; + unsafe { self.trigger_dynamic_ref_with_caller(event_id, event, MaybeLocation::caller()) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + unsafe fn trigger_dynamic_ref_with_caller( + &mut self, + event_id: ComponentId, + event_data: &mut E, + caller: MaybeLocation, + ) { + let mut world = DeferredWorld::from(self); + // SAFETY: `event_data` is accessible as the type represented by `event_id` + unsafe { + world.trigger_observers_with_data::<_, ()>( + event_id, + None, + None, + core::iter::empty::(), + event_data, + false, + caller, + ); + }; + } + + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. /// If you need to use the event after triggering it, use [`World::trigger_targets_ref`] instead. #[track_caller] - pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { + pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { self.trigger_targets_with_caller(event, targets, MaybeLocation::caller()); } - pub(crate) fn trigger_targets_with_caller( + pub(crate) fn trigger_targets_with_caller( &mut self, mut event: E, targets: impl TriggerTargets, @@ -626,19 +259,23 @@ impl World { } } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. #[track_caller] - pub fn trigger_targets_ref(&mut self, event: &mut E, targets: impl TriggerTargets) { + pub fn trigger_targets_ref( + &mut self, + event: &mut E, + targets: impl TriggerTargets, + ) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { self.trigger_targets_dynamic_ref(event_id, event, targets) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. @@ -648,7 +285,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic( + pub unsafe fn trigger_targets_dynamic( &mut self, event_id: ComponentId, mut event_data: E, @@ -660,7 +297,7 @@ impl World { }; } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets_dynamic`], this method is most useful when it's necessary to check @@ -670,7 +307,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic_ref( + pub unsafe fn trigger_targets_dynamic_ref( &mut self, event_id: ComponentId, event_data: &mut E, @@ -687,7 +324,7 @@ impl World { /// # Safety /// /// See `trigger_targets_dynamic_ref` - unsafe fn trigger_targets_dynamic_ref_with_caller( + unsafe fn trigger_targets_dynamic_ref_with_caller( &mut self, event_id: ComponentId, event_data: &mut E, @@ -702,6 +339,7 @@ impl World { world.trigger_observers_with_data::<_, E::Traversal>( event_id, None, + None, targets.components(), event_data, false, @@ -715,6 +353,7 @@ impl World { world.trigger_observers_with_data::<_, E::Traversal>( event_id, Some(target_entity), + Some(target_entity), targets.components(), event_data, E::AUTO_PROPAGATE, @@ -741,10 +380,12 @@ impl World { let descriptor = &observer_state.descriptor; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.insert(observer_entity, observer_state.runner); + cache + .global_observers + .insert(observer_entity, observer_state.runner); } else if descriptor.components.is_empty() { // Observer is not targeting any components so register it as an entity observer for &watched_entity in &observer_state.descriptor.entities { @@ -766,11 +407,16 @@ impl World { }); if descriptor.entities.is_empty() { // Register for all triggers targeting the component - observers.map.insert(observer_entity, observer_state.runner); + observers + .global_observers + .insert(observer_entity, observer_state.runner); } else { // Register for each watched entity for &watched_entity in &descriptor.entities { - let map = observers.entity_map.entry(watched_entity).or_default(); + let map = observers + .entity_component_observers + .entry(watched_entity) + .or_default(); map.insert(observer_entity, observer_state.runner); } } @@ -785,9 +431,9 @@ impl World { let observers = &mut self.observers; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.remove(&entity); + cache.global_observers.remove(&entity); } else if descriptor.components.is_empty() { for watched_entity in &descriptor.entities { // This check should be unnecessary since this observer hasn't been unregistered yet @@ -805,20 +451,24 @@ impl World { continue; }; if descriptor.entities.is_empty() { - observers.map.remove(&entity); + observers.global_observers.remove(&entity); } else { for watched_entity in &descriptor.entities { - let Some(map) = observers.entity_map.get_mut(watched_entity) else { + let Some(map) = + observers.entity_component_observers.get_mut(watched_entity) + else { continue; }; map.remove(&entity); if map.is_empty() { - observers.entity_map.remove(watched_entity); + observers.entity_component_observers.remove(watched_entity); } } } - if observers.map.is_empty() && observers.entity_map.is_empty() { + if observers.global_observers.is_empty() + && observers.entity_component_observers.is_empty() + { cache.component_observers.remove(component); if let Some(flag) = Observers::is_archetype_cached(event_type) { if let Some(by_component) = archetypes.by_component.get(component) { @@ -853,7 +503,7 @@ mod tests { use crate::component::ComponentId; use crate::{ change_detection::MaybeLocation, - observer::{Observer, OnReplace}, + observer::{Observer, Replace}, prelude::*, traversal::Traversal, }; @@ -871,10 +521,10 @@ mod tests { #[component(storage = "SparseSet")] struct S; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventA; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventWithData { counter: usize, } @@ -893,13 +543,13 @@ mod tests { struct ChildOf(Entity); impl Traversal for &'_ ChildOf { - fn traverse(item: Self::Item<'_>, _: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _: &D) -> Option { Some(item.0) } } - #[derive(Component, Event)] - #[event(traversal = &'static ChildOf, auto_propagate)] + #[derive(Component, Event, EntityEvent)] + #[entity_event(traversal = &'static ChildOf, auto_propagate)] struct EventPropagating; #[test] @@ -907,14 +557,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let entity = world.spawn(A).id(); world.despawn(entity); @@ -929,14 +577,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(A); @@ -953,14 +599,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(S); @@ -979,14 +623,12 @@ mod tests { let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut // and therefore does not automatically flush. @@ -1003,25 +645,25 @@ mod tests { let mut world = World::new(); world.init_resource::(); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); - commands.entity(obs.target().unwrap()).insert(B); + commands.entity(obs.target()).insert(B); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); - commands.entity(obs.target().unwrap()).remove::(); + commands.entity(obs.target()).remove::(); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); - commands.entity(obs.target().unwrap()).remove::(); + commands.entity(obs.target()).remove::(); }, ); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_b"); }); @@ -1039,9 +681,9 @@ mod tests { fn observer_trigger_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 1); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 2); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 4); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 1); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 2); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 4); // This flush is required for the last observer to be called when triggering the event, // due to `World::add_observer` returning `WorldEntityMut`. world.flush(); @@ -1055,13 +697,13 @@ mod tests { fn observer_trigger_targets_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 1; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 2; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 4; }); // This flush is required for the last observer to be called when triggering the event, @@ -1079,24 +721,24 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_1")); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_2")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_1")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_2")); world.spawn(A).flush(); assert_eq!(vec!["add_2", "add_1"], world.resource::().0); // Our A entity plus our two observers - assert_eq!(world.entities().len(), 3); + assert_eq!(world.entity_count(), 3); } #[test] fn observer_multiple_events() { let mut world = World::new(); world.init_resource::(); - let on_remove = OnRemove::register_component_id(&mut world); + let on_remove = Remove::register_component_id(&mut world); world.spawn( - // SAFETY: OnAdd and OnRemove are both unit types, so this is safe + // SAFETY: Add and Remove are both unit types, so this is safe unsafe { - Observer::new(|_: Trigger, mut res: ResMut| { + Observer::new(|_: On, mut res: ResMut| { res.observed("add/remove"); }) .with_event(on_remove) @@ -1118,7 +760,7 @@ mod tests { world.register_component::(); world.register_component::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1132,7 +774,7 @@ mod tests { fn observer_despawn() { let mut world = World::new(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Observer triggered after being despawned."); }; let observer = world.add_observer(system).id(); @@ -1148,11 +790,11 @@ mod tests { let entity = world.spawn((A, B)).flush(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_a"); }); - let system: fn(Trigger) = |_: Trigger| { + let system: fn(On) = |_: On| { panic!("Observer triggered after being despawned."); }; @@ -1169,7 +811,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1182,12 +824,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); - world.add_observer(move |obs: Trigger, mut res: ResMut| { - assert_eq!(obs.target(), None); + world.add_observer(move |obs: On, mut res: ResMut| { + assert_eq!(obs.target(), Entity::PLACEHOLDER); res.observed("event_a"); }); @@ -1204,17 +846,17 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("a_1")) + .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); - world.add_observer(move |obs: Trigger, mut res: ResMut| { - assert_eq!(obs.target().unwrap(), entity); + world.add_observer(move |obs: On, mut res: ResMut| { + assert_eq!(obs.target(), entity); res.observed("a_2"); }); @@ -1239,26 +881,26 @@ mod tests { // targets (entity_1, A) let entity_1 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); // targets (entity_2, B) let entity_2 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 10) + .observe(|_: On, mut res: ResMut| res.0 += 10) .id(); // targets any entity or component - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 100); + world.add_observer(|_: On, mut res: ResMut| res.0 += 100); // targets any entity, and components A or B - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 1000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 1000); // test all tuples - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 10000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 10000); world.add_observer( - |_: Trigger, mut res: ResMut| { + |_: On, mut res: ResMut| { res.0 += 100000; }, ); world.add_observer( - |_: Trigger, + |_: On, mut res: ResMut| res.0 += 1000000, ); @@ -1346,7 +988,7 @@ mod tests { let component_id = world.register_component::(); world.spawn( - Observer::new(|_: Trigger, mut res: ResMut| res.observed("event_a")) + Observer::new(|_: On, mut res: ResMut| res.observed("event_a")) .with_component(component_id), ); @@ -1366,7 +1008,7 @@ mod tests { fn observer_dynamic_trigger() { let mut world = World::new(); world.init_resource::(); - let event_a = OnRemove::register_component_id(&mut world); + let event_a = Remove::register_component_id(&mut world); // SAFETY: we registered `event_a` above and it matches the type of EventA let observe = unsafe { @@ -1390,21 +1032,27 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let parent = world - .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + let parent = world.spawn_empty().id(); + let child = world.spawn(ChildOf(parent)).id(); + + world.entity_mut(parent).observe( + move |trigger: On, mut res: ResMut| { res.observed("parent"); - }) - .id(); - let child = world - .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + assert_eq!(trigger.target(), parent); + assert_eq!(trigger.original_target(), child); + }, + ); + + world.entity_mut(child).observe( + move |trigger: On, mut res: ResMut| { res.observed("child"); - }) - .id(); + assert_eq!(trigger.target(), child); + assert_eq!(trigger.original_target(), child); + }, + ); - // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // TODO: ideally this flush is not necessary, but right now observe() returns EntityWorldMut // and therefore does not automatically flush. world.flush(); world.trigger_targets(EventPropagating, child); @@ -1419,14 +1067,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1449,14 +1097,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1479,7 +1127,7 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); @@ -1487,7 +1135,7 @@ mod tests { let child = world .spawn(ChildOf(parent)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child"); trigger.propagate(false); }, @@ -1509,21 +1157,21 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child_a = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_a"); }) .id(); let child_b = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1546,7 +1194,7 @@ mod tests { let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("event"); }) .id(); @@ -1566,7 +1214,7 @@ mod tests { let parent_a = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_a"); }) .id(); @@ -1574,7 +1222,7 @@ mod tests { let child_a = world .spawn(ChildOf(parent_a)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child_a"); trigger.propagate(false); }, @@ -1583,14 +1231,14 @@ mod tests { let parent_b = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_b"); }) .id(); let child_b = world .spawn(ChildOf(parent_b)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1611,7 +1259,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("event"); }); @@ -1633,8 +1281,8 @@ mod tests { world.init_resource::(); world.add_observer( - |trigger: Trigger, query: Query<&A>, mut res: ResMut| { - if query.get(trigger.target().unwrap()).is_ok() { + |trigger: On, query: Query<&A>, mut res: ResMut| { + if query.get(trigger.target()).is_ok() { res.observed("event"); } }, @@ -1655,9 +1303,9 @@ mod tests { // Originally for https://github.com/bevyengine/bevy/issues/18452 #[test] fn observer_modifies_relationship() { - fn on_add(trigger: Trigger, mut commands: Commands) { + fn on_add(trigger: On, mut commands: Commands) { commands - .entity(trigger.target().unwrap()) + .entity(trigger.target()) .with_related_entities::(|rsc| { rsc.spawn_empty(); }); @@ -1676,7 +1324,7 @@ mod tests { let mut world = World::new(); // Observe the removal of A - this will run during despawn - world.add_observer(|_: Trigger, mut cmd: Commands| { + world.add_observer(|_: On, mut cmd: Commands| { // Spawn a new entity - this reserves a new ID and requires a flush // afterward before Entities::free can be called. cmd.spawn_empty(); @@ -1684,7 +1332,7 @@ mod tests { let ent = world.spawn(A).id(); - // Despawn our entity, which runs the OnRemove observer and allocates a + // Despawn our entity, which runs the Remove observer and allocates a // new Entity. // Should not panic - if it does, then Entities was not flushed properly // after the observer's spawn_empty. @@ -1702,7 +1350,7 @@ mod tests { let mut world = World::new(); // This fails because `ResA` is not present in the world - world.add_observer(|_: Trigger, _: Res, mut commands: Commands| { + world.add_observer(|_: On, _: Res, mut commands: Commands| { commands.insert_resource(ResB); }); world.trigger(EventA); @@ -1715,7 +1363,7 @@ mod tests { let mut world = World::new(); world.add_observer( - |_: Trigger, mut params: ParamSet<(Query, Commands)>| { + |_: On, mut params: ParamSet<(Query, Commands)>| { params.p1().insert_resource(ResA); }, ); @@ -1736,7 +1384,7 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.trigger(EventA); @@ -1750,10 +1398,10 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.commands().spawn(Component).clear(); @@ -1771,7 +1419,7 @@ mod tests { let b_id = world.register_component::(); world.add_observer( - |trigger: Trigger, mut counter: ResMut| { + |trigger: On, mut counter: ResMut| { for &component in trigger.components() { *counter.0.entry(component).or_default() += 1; } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 61cc0973f3..acc2830a7d 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,15 +1,10 @@ -use alloc::{boxed::Box, vec}; +//! Logic for evaluating observers, and storing functions inside of observers. + use core::any::Any; use crate::{ - component::{ComponentId, Mutable, StorageType}, - error::{ErrorContext, ErrorHandler}, - lifecycle::{ComponentHook, HookContext}, - observer::{ObserverDescriptor, ObserverTrigger}, - prelude::*, - query::DebugCheckedUnwrap, - system::{IntoObserverSystem, ObserverSystem}, - world::DeferredWorld, + error::ErrorContext, observer::ObserverTrigger, prelude::*, query::DebugCheckedUnwrap, + system::ObserverSystem, world::DeferredWorld, }; use bevy_ptr::PtrMut; @@ -19,316 +14,7 @@ use bevy_ptr::PtrMut; /// but can be overridden for custom behavior. pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: &mut bool); -/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". -/// -/// Observers listen for a "trigger" of a specific [`Event`]. Events are triggered by calling [`World::trigger`] or [`World::trigger_targets`]. -/// -/// Note that "buffered" events sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. They must be triggered at a specific -/// point in the schedule. -/// -/// # Usage -/// -/// The simplest usage -/// of the observer pattern looks like this: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// #[derive(Event)] -/// struct Speak { -/// message: String, -/// } -/// -/// world.add_observer(|trigger: Trigger| { -/// println!("{}", trigger.event().message); -/// }); -/// -/// // Observers currently require a flush() to be registered. In the context of schedules, -/// // this will generally be done for you. -/// world.flush(); -/// -/// world.trigger(Speak { -/// message: "Hello!".into(), -/// }); -/// ``` -/// -/// Notice that we used [`World::add_observer`]. This is just a shorthand for spawning an [`Observer`] manually: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct Speak; -/// // These are functionally the same: -/// world.add_observer(|trigger: Trigger| {}); -/// world.spawn(Observer::new(|trigger: Trigger| {})); -/// ``` -/// -/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct PrintNames; -/// # #[derive(Component, Debug)] -/// # struct Name; -/// world.add_observer(|trigger: Trigger, names: Query<&Name>| { -/// for name in &names { -/// println!("{name:?}"); -/// } -/// }); -/// ``` -/// -/// Note that [`Trigger`] must always be the first parameter. -/// -/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct SpawnThing; -/// # #[derive(Component, Debug)] -/// # struct Thing; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// commands.spawn(Thing); -/// }); -/// ``` -/// -/// Observers can also trigger new events: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct A; -/// # #[derive(Event)] -/// # struct B; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// commands.trigger(B); -/// }); -/// ``` -/// -/// When the commands are flushed (including these "nested triggers") they will be -/// recursively evaluated until there are no commands left, meaning nested triggers all -/// evaluate at the same time! -/// -/// Events can be triggered for entities, which will be passed to the [`Observer`]: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let entity = world.spawn_empty().id(); -/// #[derive(Event)] -/// struct Explode; -/// -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// println!("Entity {} goes BOOM!", trigger.target().unwrap()); -/// commands.entity(trigger.target().unwrap()).despawn(); -/// }); -/// -/// world.flush(); -/// -/// world.trigger_targets(Explode, entity); -/// ``` -/// -/// You can trigger multiple entities at once: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let e1 = world.spawn_empty().id(); -/// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] -/// # struct Explode; -/// world.trigger_targets(Explode, [e1, e2]); -/// ``` -/// -/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # #[derive(Component, Debug)] -/// # struct Name(String); -/// # let mut world = World::default(); -/// # let e1 = world.spawn_empty().id(); -/// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] -/// # struct Explode; -/// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { -/// println!("Boom!"); -/// commands.entity(trigger.target().unwrap()).despawn(); -/// }); -/// -/// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { -/// println!("The explosion fizzles! This entity is immune!"); -/// }); -/// ``` -/// -/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. -/// This protects against observer "garbage" building up over time. -/// -/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) -/// just shorthand for spawning an [`Observer`] directly: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let entity = world.spawn_empty().id(); -/// # #[derive(Event)] -/// # struct Explode; -/// let mut observer = Observer::new(|trigger: Trigger| {}); -/// observer.watch_entity(entity); -/// world.spawn(observer); -/// ``` -/// -/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! -/// -/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. -/// serves as the "source of truth" of the observer. -/// -/// [`SystemParam`]: crate::system::SystemParam -pub struct Observer { - hook_on_add: ComponentHook, - error_handler: Option, - system: Box, - pub(crate) descriptor: ObserverDescriptor, - pub(crate) last_trigger_id: u32, - pub(crate) despawned_watched_entities: u32, - pub(crate) runner: ObserverRunner, -} - -impl Observer { - /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered - /// for _any_ entity (or no entity). - /// - /// # Panics - /// - /// Panics if the given system is an exclusive system. - pub fn new>(system: I) -> Self { - let system = Box::new(IntoObserverSystem::into_system(system)); - assert!( - !system.is_exclusive(), - concat!( - "Exclusive system `{}` may not be used as observer.\n", - "Instead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." - ), - system.name() - ); - Self { - system, - descriptor: Default::default(), - hook_on_add: hook_on_add::, - error_handler: None, - runner: observer_system_runner::, - despawned_watched_entities: 0, - last_trigger_id: 0, - } - } - - /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer - pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { - Self { - system: Box::new(|| {}), - descriptor: Default::default(), - hook_on_add: |mut world, hook_context| { - let default_error_handler = world.default_error_handler(); - world.commands().queue(move |world: &mut World| { - let entity = hook_context.entity; - if let Some(mut observe) = world.get_mut::(entity) { - if observe.descriptor.events.is_empty() { - return; - } - if observe.error_handler.is_none() { - observe.error_handler = Some(default_error_handler); - } - world.register_observer(entity); - } - }); - }, - error_handler: None, - runner, - despawned_watched_entities: 0, - last_trigger_id: 0, - } - } - - /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for the `entity`. - pub fn with_entity(mut self, entity: Entity) -> Self { - self.descriptor.entities.push(entity); - self - } - - /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for the `entity`. - /// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects. - pub fn watch_entity(&mut self, entity: Entity) { - self.descriptor.entities.push(entity); - } - - /// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// with the given component target. - pub fn with_component(mut self, component: ComponentId) -> Self { - self.descriptor.components.push(component); - self - } - - /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] - /// is triggered. - /// # Safety - /// The type of the `event` [`ComponentId`] _must_ match the actual value - /// of the event passed into the observer system. - pub unsafe fn with_event(mut self, event: ComponentId) -> Self { - self.descriptor.events.push(event); - self - } - - /// Set the error handler to use for this observer. - /// - /// See the [`error` module-level documentation](crate::error) for more information. - pub fn with_error_handler(mut self, error_handler: fn(BevyError, ErrorContext)) -> Self { - self.error_handler = Some(error_handler); - self - } - - /// Returns the [`ObserverDescriptor`] for this [`Observer`]. - pub fn descriptor(&self) -> &ObserverDescriptor { - &self.descriptor - } -} - -impl Component for Observer { - const STORAGE_TYPE: StorageType = StorageType::SparseSet; - type Mutability = Mutable; - fn on_add() -> Option { - Some(|world, context| { - let Some(observe) = world.get::(context.entity) else { - return; - }; - let hook = observe.hook_on_add; - hook(world, context); - }) - } - fn on_remove() -> Option { - Some(|mut world, HookContext { entity, .. }| { - let descriptor = core::mem::take( - &mut world - .entity_mut(entity) - .get_mut::() - .unwrap() - .as_mut() - .descriptor, - ); - world.commands().queue(move |world: &mut World| { - world.unregister_observer(entity, descriptor); - }); - }) - } -} - -fn observer_system_runner>( +pub(super) fn observer_system_runner>( mut world: DeferredWorld, observer_trigger: ObserverTrigger, ptr: PtrMut, @@ -351,7 +37,7 @@ fn observer_system_runner>( } state.last_trigger_id = last_trigger; - let trigger: Trigger = Trigger::new( + let trigger: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { ptr.deref_mut() }, propagate, @@ -362,7 +48,8 @@ fn observer_system_runner>( // - observer was triggered so must have an `Observer` component. // - observer cannot be dropped or mutated until after the system pointer is already dropped. let system: *mut dyn ObserverSystem = unsafe { - let system = state.system.downcast_mut::().debug_checked_unwrap(); + let system: &mut dyn Any = state.system.as_mut(); + let system = system.downcast_mut::().debug_checked_unwrap(); &mut *system }; @@ -411,44 +98,13 @@ fn observer_system_runner>( } } -/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). -/// -/// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters -/// erased. -/// -/// The type parameters of this function _must_ match those used to create the [`Observer`]. -/// As such, it is recommended to only use this function within the [`Observer::new`] method to -/// ensure type parameters match. -fn hook_on_add>( - mut world: DeferredWorld<'_>, - HookContext { entity, .. }: HookContext, -) { - world.commands().queue(move |world: &mut World| { - let event_id = E::register_component_id(world); - let mut components = vec![]; - B::component_ids(&mut world.components_registrator(), &mut |id| { - components.push(id); - }); - if let Some(mut observe) = world.get_mut::(entity) { - observe.descriptor.events.push(event_id); - observe.descriptor.components.extend(components); - - let system: *mut dyn ObserverSystem = observe.system.downcast_mut::().unwrap(); - // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias - unsafe { - (*system).initialize(world); - } - world.register_observer(entity); - } - }); -} #[cfg(test)] mod tests { use super::*; use crate::{ error::{ignore, DefaultErrorHandler}, event::Event, - observer::Trigger, + observer::On, }; #[derive(Event)] @@ -457,7 +113,7 @@ mod tests { #[test] #[should_panic(expected = "I failed!")] fn test_fallible_observer() { - fn system(_: Trigger) -> Result { + fn system(_: On) -> Result { Err("I failed!".into()) } @@ -472,7 +128,7 @@ mod tests { #[derive(Resource, Default)] struct Ran(bool); - fn system(_: Trigger, mut ran: ResMut) -> Result { + fn system(_: On, mut ran: ResMut) -> Result { ran.0 = true; Err("I failed!".into()) } @@ -496,11 +152,9 @@ mod tests { } #[test] - #[should_panic( - expected = "Exclusive system `bevy_ecs::observer::runner::tests::exclusive_system_cannot_be_observer::system` may not be used as observer.\nInstead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." - )] + #[should_panic] fn exclusive_system_cannot_be_observer() { - fn system(_: Trigger, _world: &mut World) {} + fn system(_: On, _world: &mut World) {} let mut world = World::default(); world.add_observer(system); } diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs new file mode 100644 index 0000000000..27d6fef5b3 --- /dev/null +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -0,0 +1,206 @@ +//! System parameters for working with observers. + +use core::marker::PhantomData; +use core::ops::DerefMut; +use core::{fmt::Debug, ops::Deref}; + +use bevy_ptr::Ptr; +use smallvec::SmallVec; + +use crate::{ + bundle::Bundle, change_detection::MaybeLocation, component::ComponentId, event::EntityEvent, + prelude::*, +}; + +/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the +/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also +/// contains event propagation information. See [`On::propagate`] for more information. +/// +/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. +/// The entity involved *does not* have to have these components, but the observer will only be +/// triggered if the event matches the components in `B`. +/// +/// This is used to to avoid providing a generic argument in your event, as is done for [`Add`] +/// and the other lifecycle events. +/// +/// Providing multiple components in this bundle will cause this event to be triggered by any +/// matching component in the bundle, +/// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). +pub struct On<'w, E, B: Bundle = ()> { + event: &'w mut E, + propagate: &'w mut bool, + trigger: ObserverTrigger, + _marker: PhantomData, +} + +/// Deprecated in favor of [`On`]. +#[deprecated(since = "0.17.0", note = "Renamed to `On`.")] +pub type Trigger<'w, E, B = ()> = On<'w, E, B>; + +impl<'w, E, B: Bundle> On<'w, E, B> { + /// Creates a new instance of [`On`] for the given event and observer information. + pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { + Self { + event, + propagate, + trigger, + _marker: PhantomData, + } + } + + /// Returns the event type of this [`On`] instance. + pub fn event_type(&self) -> ComponentId { + self.trigger.event_type + } + + /// Returns a reference to the triggered event. + pub fn event(&self) -> &E { + self.event + } + + /// Returns a mutable reference to the triggered event. + pub fn event_mut(&mut self) -> &mut E { + self.event + } + + /// Returns a pointer to the triggered event. + pub fn event_ptr(&self) -> Ptr { + Ptr::from(&self.event) + } + + /// Returns the components that triggered the observer, out of the + /// components defined in `B`. Does not necessarily include all of them as + /// `B` acts like an `OR` filter rather than an `AND` filter. + pub fn components(&self) -> &[ComponentId] { + &self.trigger.components + } + + /// Returns the [`Entity`] that observed the triggered event. + /// This allows you to despawn the observer, ceasing observation. + /// + /// # Examples + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(Event, EntityEvent)] + /// struct AssertEvent; + /// + /// fn assert_observer(trigger: On) { + /// assert_eq!(trigger.observer(), trigger.target()); + /// } + /// + /// let mut world = World::new(); + /// let observer = world.spawn(Observer::new(assert_observer)).id(); + /// + /// world.trigger_targets(AssertEvent, observer); + /// ``` + pub fn observer(&self) -> Entity { + self.trigger.observer + } + + /// Returns the source code location that triggered this observer. + pub fn caller(&self) -> MaybeLocation { + self.trigger.caller + } +} + +impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { + /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. + /// + /// Note that if event propagation is enabled, this may not be the same as the original target of the event, + /// which can be accessed via [`On::original_target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn target(&self) -> Entity { + self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) + } + + /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. + /// + /// If event propagation is not enabled, this will always return the same value as [`On::target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn original_target(&self) -> Entity { + self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) + } + + /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. + /// + /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events + /// use `()` which ends the path immediately and prevents propagation. + /// + /// To enable propagation, you must: + /// + Set [`EntityEvent::Traversal`] to the component you want to propagate along. + /// + Either call `propagate(true)` in the first observer or set [`EntityEvent::AUTO_PROPAGATE`] to `true`. + /// + /// You can prevent an event from propagating further using `propagate(false)`. + /// + /// [`Traversal`]: crate::traversal::Traversal + pub fn propagate(&mut self, should_propagate: bool) { + *self.propagate = should_propagate; + } + + /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. + /// + /// [`propagate`]: On::propagate + pub fn get_propagate(&self) -> bool { + *self.propagate + } +} + +impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("On") + .field("event", &self.event) + .field("propagate", &self.propagate) + .field("trigger", &self.trigger) + .field("_marker", &self._marker) + .finish() + } +} + +impl<'w, E, B: Bundle> Deref for On<'w, E, B> { + type Target = E; + + fn deref(&self) -> &Self::Target { + self.event + } +} + +impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.event + } +} + +/// Metadata about a specific [`Event`] that triggered an observer. +/// +/// This information is exposed via methods on [`On`]. +#[derive(Debug)] +pub struct ObserverTrigger { + /// The [`Entity`] of the observer handling the trigger. + pub observer: Entity, + /// The [`Event`] the trigger targeted. + pub event_type: ComponentId, + /// The [`ComponentId`]s the trigger targeted. + pub components: SmallVec<[ComponentId; 2]>, + /// The entity that the entity-event targeted, if any. + /// + /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. + pub current_target: Option, + /// The entity that the entity-event was originally targeted at, if any. + /// + /// If event propagation is enabled, this will be the first entity that the event was targeted at, + /// even if the event was propagated to other entities. + pub original_target: Option, + /// The location of the source code that triggered the observer. + pub caller: MaybeLocation, +} + +impl ObserverTrigger { + /// Returns the components that the trigger targeted. + pub fn components(&self) -> &[ComponentId] { + &self.components + } +} diff --git a/crates/bevy_ecs/src/observer/trigger_targets.rs b/crates/bevy_ecs/src/observer/trigger_targets.rs new file mode 100644 index 0000000000..77728e4acd --- /dev/null +++ b/crates/bevy_ecs/src/observer/trigger_targets.rs @@ -0,0 +1,117 @@ +//! Stores the [`TriggerTargets`] trait. + +use crate::{component::ComponentId, prelude::*}; +use alloc::vec::Vec; +use variadics_please::all_tuples; + +/// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. +/// +/// When an event is triggered with [`TriggerTargets`], any [`Observer`] that watches for that specific +/// event-target combination will run. +/// +/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. +/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, +/// allowing you to trigger events for multiple targets at once. +pub trait TriggerTargets { + /// The components the trigger should target. + fn components(&self) -> impl Iterator + Clone + '_; + + /// The entities the trigger should target. + fn entities(&self) -> impl Iterator + Clone + '_; +} + +impl TriggerTargets for &T { + fn components(&self) -> impl Iterator + Clone + '_ { + (**self).components() + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + (**self).entities() + } +} + +impl TriggerTargets for Entity { + fn components(&self) -> impl Iterator + Clone + '_ { + [].into_iter() + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + core::iter::once(*self) + } +} + +impl TriggerTargets for ComponentId { + fn components(&self) -> impl Iterator + Clone + '_ { + core::iter::once(*self) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + [].into_iter() + } +} + +impl TriggerTargets for Vec { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +impl TriggerTargets for [T; N] { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +impl TriggerTargets for [T] { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +macro_rules! impl_trigger_targets_tuples { + ($(#[$meta:meta])* $($trigger_targets: ident),*) => { + #[expect(clippy::allow_attributes, reason = "can't guarantee violation of non_snake_case")] + #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] + $(#[$meta])* + impl<$($trigger_targets: TriggerTargets),*> TriggerTargets for ($($trigger_targets,)*) + { + fn components(&self) -> impl Iterator + Clone + '_ { + let iter = [].into_iter(); + let ($($trigger_targets,)*) = self; + $( + let iter = iter.chain($trigger_targets.components()); + )* + iter + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + let iter = [].into_iter(); + let ($($trigger_targets,)*) = self; + $( + let iter = iter.chain($trigger_targets.entities()); + )* + iter + } + } + } +} + +all_tuples!( + #[doc(fake_variadic)] + impl_trigger_targets_tuples, + 0, + 15, + T +); diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 9c63cb5a74..0c5b29f715 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -4,7 +4,6 @@ use crate::world::World; use alloc::{format, string::String, vec, vec::Vec}; use core::{fmt, fmt::Debug, marker::PhantomData}; use derive_more::From; -use disqualified::ShortName; use fixedbitset::FixedBitSet; use thiserror::Error; @@ -999,12 +998,11 @@ impl AccessConflicts { .map(|index| { format!( "{}", - ShortName( - &world - .components - .get_name(ComponentId::get_sparse_set_index(index)) - .unwrap() - ) + world + .components + .get_name(ComponentId::get_sparse_set_index(index)) + .unwrap() + .shortname() ) }) .collect::>() diff --git a/crates/bevy_ecs/src/query/error.rs b/crates/bevy_ecs/src/query/error.rs index 6d0b149b86..fd431f4be1 100644 --- a/crates/bevy_ecs/src/query/error.rs +++ b/crates/bevy_ecs/src/query/error.rs @@ -1,3 +1,4 @@ +use bevy_utils::prelude::DebugName; use thiserror::Error; use crate::{ @@ -54,10 +55,10 @@ impl core::fmt::Display for QueryEntityError { pub enum QuerySingleError { /// No entity fits the query. #[error("No entities fit the query {0}")] - NoEntities(&'static str), + NoEntities(DebugName), /// Multiple entities fit the query. #[error("Multiple entities fit the query {0}")] - MultipleEntities(&'static str), + MultipleEntities(DebugName), } #[cfg(test)] diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 55fa42c41e..2564223972 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -12,6 +12,7 @@ use crate::{ }, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; use smallvec::SmallVec; use variadics_please::all_tuples; @@ -163,7 +164,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryItem` is only available when accessing the query with mutable methods. -/// impl<'w> HealthQueryItem<'w> { +/// impl<'w, 's> HealthQueryItem<'w, 's> { /// fn damage(&mut self, value: f32) { /// self.health.0 -= value; /// } @@ -174,7 +175,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryReadOnlyItem` is only available when accessing the query with immutable methods. -/// impl<'w> HealthQueryReadOnlyItem<'w> { +/// impl<'w, 's> HealthQueryReadOnlyItem<'w, 's> { /// fn total(&self) -> f32 { /// self.health.0 + self.buff.map_or(0.0, |Buff(buff)| *buff) /// } @@ -290,10 +291,12 @@ pub unsafe trait QueryData: WorldQuery { /// The item returned by this [`WorldQuery`] /// This will be the data retrieved by the query, /// and is visible to the end user when calling e.g. `Query::get`. - type Item<'a>; + type Item<'w, 's>; /// This function manually implements subtyping for the query items. - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort>; + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's>; /// Offers additional access above what we requested in `update_component_access`. /// Implementations may add additional access that is a subset of `available_access` @@ -322,11 +325,12 @@ pub unsafe trait QueryData: WorldQuery { /// - Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. /// - There must not be simultaneous conflicting component access registered in `update_component_access`. - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w>; + ) -> Self::Item<'w, 's>; } /// A [`QueryData`] that is read only. @@ -337,9 +341,20 @@ pub unsafe trait QueryData: WorldQuery { pub unsafe trait ReadOnlyQueryData: QueryData {} /// The item type returned when a [`WorldQuery`] is iterated over -pub type QueryItem<'w, Q> = ::Item<'w>; +pub type QueryItem<'w, 's, Q> = ::Item<'w, 's>; /// The read-only variant of the item type returned when a [`QueryData`] is iterated over immutably -pub type ROQueryItem<'w, D> = QueryItem<'w, ::ReadOnly>; +pub type ROQueryItem<'w, 's, D> = QueryItem<'w, 's, ::ReadOnly>; + +/// A [`QueryData`] that does not borrow from its [`QueryState`](crate::query::QueryState). +/// +/// This is implemented by most `QueryData` types. +/// The main exceptions are [`FilteredEntityRef`], [`FilteredEntityMut`], [`EntityRefExcept`], and [`EntityMutExcept`], +/// which borrow an access list from their query state. +/// Consider using a full [`EntityRef`] or [`EntityMut`] if you would need those. +pub trait ReleaseStateQueryData: QueryData { + /// Releases the borrow from the query state by converting an item to have a `'static` state lifetime. + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static>; +} /// SAFETY: /// `update_component_access` does nothing. @@ -350,9 +365,9 @@ unsafe impl WorldQuery for Entity { fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -361,16 +376,20 @@ unsafe impl WorldQuery for Entity { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -394,18 +413,21 @@ unsafe impl QueryData for Entity { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { entity } } @@ -413,6 +435,12 @@ unsafe impl QueryData for Entity { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for Entity {} +impl ReleaseStateQueryData for Entity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. @@ -424,9 +452,9 @@ unsafe impl WorldQuery for EntityLocation { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -438,16 +466,20 @@ unsafe impl WorldQuery for EntityLocation { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -470,18 +502,21 @@ unsafe impl WorldQuery for EntityLocation { unsafe impl QueryData for EntityLocation { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityLocation; + type Item<'w, 's> = EntityLocation; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world unsafe { fetch.get(entity).debug_checked_unwrap() } } @@ -490,6 +525,12 @@ unsafe impl QueryData for EntityLocation { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityLocation {} +impl ReleaseStateQueryData for EntityLocation { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `SpawnDetails` query parameter fetches the [`Tick`] the entity was spawned at. /// /// To evaluate whether the spawn happened since the last time the system ran, the system @@ -570,9 +611,9 @@ unsafe impl WorldQuery for SpawnDetails { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -586,16 +627,20 @@ unsafe impl WorldQuery for SpawnDetails { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -620,18 +665,21 @@ unsafe impl WorldQuery for SpawnDetails { unsafe impl QueryData for SpawnDetails { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Self; + type Item<'w, 's> = Self; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: only living entities are queried let (spawned_by, spawned_at) = unsafe { fetch @@ -650,6 +698,12 @@ unsafe impl QueryData for SpawnDetails { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for SpawnDetails {} +impl ReleaseStateQueryData for SpawnDetails { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for WorldQueries that can fetch multiple components from an entity /// ([`EntityRef`], [`EntityMut`], etc.) #[derive(Copy, Clone)] @@ -672,9 +726,9 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -688,16 +742,20 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -726,18 +784,21 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { unsafe impl<'a> QueryData for EntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRef<'w>; + type Item<'w, 's> = EntityRef<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -753,6 +814,12 @@ unsafe impl<'a> QueryData for EntityRef<'a> { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityRef<'_> {} +impl ReleaseStateQueryData for EntityRef<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for EntityMut<'a> { type Fetch<'w> = EntityFetch<'w>; @@ -762,9 +829,9 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -778,16 +845,20 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -816,18 +887,21 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { unsafe impl<'a> QueryData for EntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRef<'a>; - type Item<'w> = EntityMut<'w>; + type Item<'w, 's> = EntityMut<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -840,6 +914,12 @@ unsafe impl<'a> QueryData for EntityMut<'a> { } } +impl ReleaseStateQueryData for EntityMut<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { type Fetch<'w> = (EntityFetch<'w>, Access); @@ -851,9 +931,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { const IS_DENSE: bool = false; - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -870,9 +950,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -880,7 +960,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -915,9 +995,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { unsafe impl<'a> QueryData for FilteredEntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = FilteredEntityRef<'w>; + type Item<'w, 's> = FilteredEntityRef<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } @@ -941,11 +1023,12 @@ unsafe impl<'a> QueryData for FilteredEntityRef<'a> { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -972,9 +1055,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { const IS_DENSE: bool = false; - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -991,9 +1074,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -1001,7 +1084,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -1036,9 +1119,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { unsafe impl<'a> QueryData for FilteredEntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = FilteredEntityRef<'a>; - type Item<'w> = FilteredEntityMut<'w>; + type Item<'w, 's> = FilteredEntityMut<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } @@ -1060,11 +1145,12 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -1091,9 +1177,9 @@ where fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1106,15 +1192,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _: &mut Self::Fetch<'w>, - _: &Self::State, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1130,7 +1216,7 @@ where assert!( access.is_compatible(&my_access), "`EntityRefExcept<{}>` conflicts with a previous access in this query.", - core::any::type_name::(), + DebugName::type_name::(), ); access.extend(&my_access); } @@ -1161,17 +1247,20 @@ where { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRefExcept<'w, B>; + type Item<'w, 's> = EntityRefExcept<'w, B>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1198,9 +1287,9 @@ where fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1213,15 +1302,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _: &mut Self::Fetch<'w>, - _: &Self::State, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1237,7 +1326,7 @@ where assert!( access.is_compatible(&my_access), "`EntityMutExcept<{}>` conflicts with a previous access in this query.", - core::any::type_name::() + DebugName::type_name::() ); access.extend(&my_access); } @@ -1269,17 +1358,20 @@ where { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRefExcept<'a, B>; - type Item<'w> = EntityMutExcept<'w, B>; + type Item<'w, 's> = EntityMutExcept<'w, B>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1299,9 +1391,9 @@ unsafe impl WorldQuery for &Archetype { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -1313,16 +1405,20 @@ unsafe impl WorldQuery for &Archetype { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -1345,18 +1441,21 @@ unsafe impl WorldQuery for &Archetype { unsafe impl QueryData for &Archetype { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w Archetype; + type Item<'w, 's> = &'w Archetype; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let (entities, archetypes) = *fetch; // SAFETY: `fetch` must be called with an entity that exists in the world let location = unsafe { entities.get(entity).debug_checked_unwrap() }; @@ -1368,6 +1467,12 @@ unsafe impl QueryData for &Archetype { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &Archetype {} +impl ReleaseStateQueryData for &Archetype { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `& T`. pub struct ReadFetch<'w, T: Component> { components: StorageSwitch< @@ -1384,6 +1489,7 @@ impl Clone for ReadFetch<'_, T> { *self } } + impl Copy for ReadFetch<'_, T> {} /// SAFETY: @@ -1400,7 +1506,7 @@ unsafe impl WorldQuery for &T { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, _last_run: Tick, @@ -1465,7 +1571,7 @@ unsafe impl WorldQuery for &T { assert!( !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_read(component_id); } @@ -1490,18 +1596,21 @@ unsafe impl WorldQuery for &T { unsafe impl QueryData for &T { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w T; + type Item<'w, 's> = &'w T; - fn shrink<'wlong: 'wshort, 'wshort>(item: &'wlong T) -> &'wshort T { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1527,6 +1636,12 @@ unsafe impl QueryData for &T { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &T {} +impl ReleaseStateQueryData for &T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + #[doc(hidden)] pub struct RefFetch<'w, T: Component> { components: StorageSwitch< @@ -1551,6 +1666,7 @@ impl Clone for RefFetch<'_, T> { *self } } + impl Copy for RefFetch<'_, T> {} /// SAFETY: @@ -1567,7 +1683,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1641,7 +1757,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { assert!( !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_read(component_id); } @@ -1666,18 +1782,21 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Ref<'w, T>; + type Item<'w, 's> = Ref<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Ref<'wlong, T>) -> Ref<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1726,6 +1845,12 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { /// SAFETY: access is read only unsafe impl<'__w, T: Component> ReadOnlyQueryData for Ref<'__w, T> {} +impl ReleaseStateQueryData for Ref<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `&mut T`. pub struct WriteFetch<'w, T: Component> { components: StorageSwitch< @@ -1750,6 +1875,7 @@ impl Clone for WriteFetch<'_, T> { *self } } + impl Copy for WriteFetch<'_, T> {} /// SAFETY: @@ -1766,7 +1892,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1840,7 +1966,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { assert!( !access.access().has_component_read(component_id), "&mut {} conflicts with a previous access in this query. Mutable component access must be unique.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_write(component_id); } @@ -1865,18 +1991,21 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { unsafe impl<'__w, T: Component> QueryData for &'__w mut T { const IS_READ_ONLY: bool = false; type ReadOnly = &'__w T; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1922,6 +2051,12 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T } } +impl> ReleaseStateQueryData for &mut T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// When `Mut` is used in a query, it will be converted to `Ref` when transformed into its read-only form, providing access to change detection methods. /// /// By contrast `&mut T` will result in a `Mut` item in mutable form to record mutations, but result in a bare `&T` in read-only form. @@ -1941,7 +2076,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { #[inline] // Forwarded to `&mut T` - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, state: &ComponentId, last_run: Tick, @@ -1980,7 +2115,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { assert!( !access.access().has_component_read(component_id), "Mut<{}> conflicts with a previous access in this query. Mutable component access mut be unique.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_write(component_id); } @@ -2008,23 +2143,32 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Mut<'__w, T> { const IS_READ_ONLY: bool = false; type ReadOnly = Ref<'__w, T>; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; // Forwarded to `&mut T` - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { <&mut T as QueryData>::shrink(item) } #[inline(always)] // Forwarded to `&mut T` - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, // Rust complains about lifetime bounds not matching the trait if I directly use `WriteFetch<'w, T>` right here. // But it complains nowhere else in the entire trait implementation. fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Mut<'w, T> { - <&mut T as QueryData>::fetch(fetch, entity, table_row) + ) -> Self::Item<'w, 's> { + <&mut T as QueryData>::fetch(state, fetch, entity, table_row) + } +} + +impl> ReleaseStateQueryData for Mut<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item } } @@ -2059,9 +2203,9 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &T::State, + state: &'s T::State, last_run: Tick, this_run: Tick, ) -> OptionFetch<'w, T> { @@ -2075,9 +2219,9 @@ unsafe impl WorldQuery for Option { const IS_DENSE: bool = T::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut OptionFetch<'w, T>, - state: &T::State, + state: &'s T::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -2091,7 +2235,11 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn set_table<'w>(fetch: &mut OptionFetch<'w, T>, state: &T::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut OptionFetch<'w, T>, + state: &'s T::State, + table: &'w Table, + ) { fetch.matches = T::matches_component_set(state, &|id| table.has_column(id)); if fetch.matches { // SAFETY: The invariants are upheld by the caller. @@ -2136,28 +2284,37 @@ unsafe impl WorldQuery for Option { unsafe impl QueryData for Option { const IS_READ_ONLY: bool = T::IS_READ_ONLY; type ReadOnly = Option; - type Item<'w> = Option>; + type Item<'w, 's> = Option>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item.map(T::shrink) } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch .matches // SAFETY: The invariants are upheld by the caller. - .then(|| unsafe { T::fetch(&mut fetch.fetch, entity, table_row) }) + .then(|| unsafe { T::fetch(state, &mut fetch.fetch, entity, table_row) }) } } /// SAFETY: [`OptionFetch`] is read only because `T` is read only unsafe impl ReadOnlyQueryData for Option {} +impl ReleaseStateQueryData for Option { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item.map(T::release_state) + } +} + /// Returns a bool that describes if an entity has the component `T`. /// /// This can be used in a [`Query`](crate::system::Query) if you want to know whether or not entities @@ -2225,7 +2382,7 @@ pub struct Has(PhantomData); impl core::fmt::Debug for Has { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - write!(f, "Has<{}>", core::any::type_name::()) + write!(f, "Has<{}>", DebugName::type_name::()) } } @@ -2241,9 +2398,9 @@ unsafe impl WorldQuery for Has { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -2258,9 +2415,9 @@ unsafe impl WorldQuery for Has { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, _table: &Table, ) { @@ -2268,7 +2425,11 @@ unsafe impl WorldQuery for Has { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &'s Self::State, + table: &'w Table, + ) { *fetch = table.has_column(*state); } @@ -2300,18 +2461,21 @@ unsafe impl WorldQuery for Has { unsafe impl QueryData for Has { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = bool; + type Item<'w, 's> = bool; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { *fetch } } @@ -2319,6 +2483,12 @@ unsafe impl QueryData for Has { /// SAFETY: [`Has`] is read only unsafe impl ReadOnlyQueryData for Has {} +impl ReleaseStateQueryData for Has { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// /// `Query>` is equivalent to `Query<(Option<&A>, Option<&B>, Option<&mut C>), Or<(With, With, With)>>`. @@ -2327,7 +2497,7 @@ unsafe impl ReadOnlyQueryData for Has {} pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $item: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -2349,9 +2519,9 @@ macro_rules! impl_tuple_query_data { unsafe impl<$($name: QueryData),*> QueryData for ($($name,)*) { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = ($($name::ReadOnly,)*); - type Item<'w> = ($($name::Item<'w>,)*); + type Item<'w, 's> = ($($name::Item<'w, 's>,)*); - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>(item: Self::Item<'wlong, 's>) -> Self::Item<'wshort, 's> { let ($($name,)*) = item; ($( $name::shrink($name), @@ -2369,26 +2539,40 @@ macro_rules! impl_tuple_query_data { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - ($(unsafe { $name::fetch($name, entity, table_row) },)*) + ($(unsafe { $name::fetch($state, $name, entity, table_row) },)*) } } - $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for ($($name,)*) {} + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for ($($name,)*) { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($name::release_state($item),)*) + } + } }; } macro_rules! impl_anytuple_fetch { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident, $item: ident)),*) => { $(#[$meta])* #[expect( clippy::allow_attributes, @@ -2423,7 +2607,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn init_fetch<'w>(_world: UnsafeWorldCell<'w>, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(_world: UnsafeWorldCell<'w>, state: &'s Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(( unsafe { $name::init_fetch(_world, $name, _last_run, _this_run) }, false),)*) @@ -2432,9 +2616,9 @@ macro_rules! impl_anytuple_fetch { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table ) { @@ -2450,7 +2634,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table) { let ($($name,)*) = _fetch; let ($($state,)*) = _state; $( @@ -2522,9 +2706,9 @@ macro_rules! impl_anytuple_fetch { unsafe impl<$($name: QueryData),*> QueryData for AnyOf<($($name,)*)> { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = AnyOf<($($name::ReadOnly,)*)>; - type Item<'w> = ($(Option<$name::Item<'w>>,)*); + type Item<'w, 's> = ($(Option<$name::Item<'w, 's>>,)*); - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>(item: Self::Item<'wlong, 's>) -> Self::Item<'wshort, 's> { let ($($name,)*) = item; ($( $name.map($name::shrink), @@ -2532,15 +2716,17 @@ macro_rules! impl_anytuple_fetch { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let ($($name,)*) = _fetch; + let ($($state,)*) = _state; ($( // SAFETY: The invariants are required to be upheld by the caller. - $name.1.then(|| unsafe { $name::fetch(&mut $name.0, _entity, _table_row) }), + $name.1.then(|| unsafe { $name::fetch($state, &mut $name.0, _entity, _table_row) }), )*) } } @@ -2548,6 +2734,20 @@ macro_rules! impl_anytuple_fetch { $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for AnyOf<($($name,)*)> {} + + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for AnyOf<($($name,)*)> { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($item.map(|$item| $name::release_state($item)),)*) + } + } }; } @@ -2557,7 +2757,8 @@ all_tuples!( 0, 15, F, - S + i, + s ); all_tuples!( #[doc(fake_variadic)] @@ -2565,7 +2766,8 @@ all_tuples!( 0, 15, F, - S + S, + i ); /// [`WorldQuery`] used to nullify queries by turning `Query` into `Query>` @@ -2580,7 +2782,8 @@ unsafe impl WorldQuery for NopWorldQuery { type Fetch<'w> = (); type State = D::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + } #[inline(always)] unsafe fn init_fetch( @@ -2627,36 +2830,44 @@ unsafe impl WorldQuery for NopWorldQuery { unsafe impl QueryData for NopWorldQuery { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `NopFetch` never accesses any data unsafe impl ReadOnlyQueryData for NopWorldQuery {} +impl ReleaseStateQueryData for NopWorldQuery { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for PhantomData { - type Fetch<'a> = (); + type Fetch<'w> = (); type State = (); fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -2666,15 +2877,19 @@ unsafe impl WorldQuery for PhantomData { // are stored in a Table (vacuous truth). const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -2697,21 +2912,29 @@ unsafe impl WorldQuery for PhantomData { unsafe impl QueryData for PhantomData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'a> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `PhantomData` never accesses any world data. unsafe impl ReadOnlyQueryData for PhantomData {} +impl ReleaseStateQueryData for PhantomData { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// A compile-time checked union of two different types that differs based on the /// [`StorageType`] of a given component. pub(super) union StorageSwitch { @@ -2828,6 +3051,124 @@ mod tests { assert_is_system(ignored_system); } + #[test] + fn derive_release_state() { + struct NonReleaseQueryData; + + /// SAFETY: + /// `update_component_access` and `update_archetype_component_access` do nothing. + /// This is sound because `fetch` does not access components. + unsafe impl WorldQuery for NonReleaseQueryData { + type Fetch<'w> = (); + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + _: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { + } + + unsafe fn init_fetch<'w, 's>( + _world: UnsafeWorldCell<'w>, + _state: &'s Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _archetype: &'w Archetype, + _table: &Table, + ) { + } + + #[inline] + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { + } + + fn update_component_access( + _state: &Self::State, + _access: &mut FilteredAccess, + ) { + } + + fn init_state(_world: &mut World) {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } + } + + /// SAFETY: `Self` is the same as `Self::ReadOnly` + unsafe impl QueryData for NonReleaseQueryData { + type ReadOnly = Self; + const IS_READ_ONLY: bool = true; + + type Item<'w, 's> = (); + + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } + + #[inline(always)] + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, + _entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w, 's> { + } + } + + /// SAFETY: access is read only + unsafe impl ReadOnlyQueryData for NonReleaseQueryData {} + + #[derive(QueryData)] + pub struct DerivedNonReleaseRead { + non_release: NonReleaseQueryData, + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedNonReleaseMutable { + non_release: NonReleaseQueryData, + a: &'static mut A, + } + + #[derive(QueryData)] + pub struct DerivedReleaseRead { + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedReleaseMutable { + a: &'static mut A, + } + + fn assert_is_release_state() {} + + assert_is_release_state::(); + assert_is_release_state::(); + } + // Ensures that each field of a `WorldQuery` struct's read-only variant // has the same visibility as its corresponding mutable field. #[test] diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index eccc819ca7..f9f4861b79 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -7,6 +7,7 @@ use crate::{ world::{unsafe_world_cell::UnsafeWorldCell, World}, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, marker::PhantomData}; use variadics_please::all_tuples; @@ -103,6 +104,7 @@ pub unsafe trait QueryFilter: WorldQuery { /// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -204,6 +206,7 @@ unsafe impl QueryFilter for With { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, @@ -304,6 +307,7 @@ unsafe impl QueryFilter for Without { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, @@ -400,7 +404,7 @@ macro_rules! impl_or_query_filter { const IS_DENSE: bool = true $(&& $filter::IS_DENSE)*; #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($filter,)*) = state; ($(OrFetch { // SAFETY: The invariants are upheld by the caller. @@ -410,7 +414,7 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($filter,)*) = fetch; let ($($state,)*) = state; $( @@ -423,9 +427,9 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: & Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -495,20 +499,22 @@ macro_rules! impl_or_query_filter { #[inline(always)] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($filter,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - false $(|| ($filter.matches && unsafe { $filter::filter_fetch(&mut $filter.fetch, entity, table_row) }))* + false $(|| ($filter.matches && unsafe { $filter::filter_fetch($state, &mut $filter.fetch, entity, table_row) }))* } } }; } macro_rules! impl_tuple_query_filter { - ($(#[$meta:meta])* $($name: ident),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -528,13 +534,15 @@ macro_rules! impl_tuple_query_filter { #[inline(always)] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - true $(&& unsafe { $name::filter_fetch($name, entity, table_row) })* + true $(&& unsafe { $name::filter_fetch($state, $name, entity, table_row) })* } } @@ -546,7 +554,8 @@ all_tuples!( impl_tuple_query_filter, 0, 15, - F + F, + S ); all_tuples!( #[doc(fake_variadic)] @@ -609,7 +618,12 @@ unsafe impl QueryFilter for Allows { const IS_ARCHETYPAL: bool = true; #[inline(always)] - unsafe fn filter_fetch(_: &mut Self::Fetch<'_>, _: Entity, _: TableRow) -> bool { + unsafe fn filter_fetch( + _: &Self::State, + _: &mut Self::Fetch<'_>, + _: Entity, + _: TableRow, + ) -> bool { true } } @@ -718,9 +732,9 @@ unsafe impl WorldQuery for Added { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -748,9 +762,9 @@ unsafe impl WorldQuery for Added { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -763,9 +777,9 @@ unsafe impl WorldQuery for Added { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -781,7 +795,7 @@ unsafe impl WorldQuery for Added { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { if access.access().has_component_write(id) { - panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",core::any::type_name::()); + panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::()); } access.add_component_read(id); } @@ -807,6 +821,7 @@ unsafe impl QueryFilter for Added { const IS_ARCHETYPAL: bool = false; #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -944,9 +959,9 @@ unsafe impl WorldQuery for Changed { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -974,9 +989,9 @@ unsafe impl WorldQuery for Changed { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -989,9 +1004,9 @@ unsafe impl WorldQuery for Changed { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -1007,7 +1022,7 @@ unsafe impl WorldQuery for Changed { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { if access.access().has_component_write(id) { - panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",core::any::type_name::()); + panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::()); } access.add_component_read(id); } @@ -1034,6 +1049,7 @@ unsafe impl QueryFilter for Changed { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -1141,9 +1157,9 @@ unsafe impl WorldQuery for Spawned { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &(), + _state: &'s (), last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1157,16 +1173,16 @@ unsafe impl WorldQuery for Spawned { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &(), + _state: &'s (), _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &(), _table: &'w Table) {} + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s (), _table: &'w Table) {} #[inline] fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} @@ -1188,6 +1204,7 @@ unsafe impl QueryFilter for Spawned { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, _table_row: TableRow, @@ -1223,6 +1240,7 @@ unsafe impl QueryFilter for Spawned { pub trait ArchetypeFilter: QueryFilter {} impl ArchetypeFilter for With {} + impl ArchetypeFilter for Without {} macro_rules! impl_archetype_filter_tuple { diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index baf0d72697..eb49204434 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -140,7 +140,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { range: Option>, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if self.cursor.is_dense { // SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids. @@ -203,7 +203,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if table.is_empty() { return accum; @@ -225,14 +225,26 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let fetched = unsafe { !F::filter_fetch(&mut self.cursor.filter, *entity, row) }; + let fetched = unsafe { + !F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + *entity, + row, + ) + }; if fetched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, *entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + *entity, + row, + ); accum = func(accum, item); } @@ -255,7 +267,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { indices: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; @@ -283,6 +295,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let fetched = unsafe { !F::filter_fetch( + &self.query_state.filter_state, &mut self.cursor.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -296,6 +309,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let item = unsafe { D::fetch( + &self.query_state.fetch_state, &mut self.cursor.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -324,7 +338,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; @@ -356,14 +370,26 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let filter_matched = unsafe { F::filter_fetch(&mut self.cursor.filter, entity, row) }; + let filter_matched = unsafe { + F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + entity, + row, + ) + }; if !filter_matched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + entity, + row, + ); accum = func(accum, item); } @@ -492,7 +518,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -549,7 +575,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -605,7 +631,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -637,7 +663,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -729,7 +755,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -762,7 +788,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -797,7 +823,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -827,7 +853,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedIter< 'w, 's, @@ -856,7 +882,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { .map(|(key, entity)| (key, NeutralOrd(entity))) .collect(); f(&mut keyed_query); - let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity.0); + let entity_iter = keyed_query + .into_iter() + .map(|(.., entity)| entity.0) + .collect::>() + .into_iter(); // SAFETY: // `self.world` has permission to access the required components. // Each lens query item is dropped before the respective actual query item is accessed. @@ -873,7 +903,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1010,7 +1040,7 @@ where /// # Safety /// `entity` must stem from `self.entity_iter`, and not have been passed before. #[inline(always)] - unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w> { + unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1039,7 +1069,14 @@ where // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - unsafe { D::fetch(&mut self.fetch, entity, location.table_row) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } } @@ -1048,7 +1085,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> Iterator where I: Iterator, { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1170,7 +1207,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> fetch: &mut D::Fetch<'w>, filter: &mut F::Fetch<'w>, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { for entity_borrow in entity_iter { let entity = entity_borrow.entity(); let Some(location) = entities.get(entity) else { @@ -1200,11 +1237,20 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // SAFETY: set_archetype was called prior. // `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d - if unsafe { F::filter_fetch(filter, entity, location.table_row) } { + if unsafe { + F::filter_fetch( + &query_state.filter_state, + filter, + entity, + location.table_row, + ) + } { // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - return Some(unsafe { D::fetch(fetch, entity, location.table_row) }); + return Some(unsafe { + D::fetch(&query_state.fetch_state, fetch, entity, location.table_row) + }); } } None @@ -1212,7 +1258,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1336,7 +1382,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -1394,7 +1440,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -1451,7 +1497,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1482,7 +1528,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1576,7 +1622,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1608,7 +1654,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1642,7 +1688,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1671,7 +1717,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedManyIter< 'w, 's, @@ -1721,7 +1767,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator Option> { + pub fn fetch_next_back(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1745,7 +1791,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator> Iterator for QueryManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1861,7 +1907,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> Iterator for QueryManyUniqueIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1954,7 +2000,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// It is always safe for shared access. /// `entity` must stem from `self.entity_iter`, and not have been passed before. #[inline(always)] - unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w> { + unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1983,12 +2029,19 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - unsafe { D::fetch(&mut self.fetch, entity, location.table_row) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { let entity = self.entity_iter.next()?; // SAFETY: @@ -2007,7 +2060,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator { /// Get next result from the query #[inline(always)] - pub fn fetch_next_back(&mut self) -> Option> { + pub fn fetch_next_back(&mut self) -> Option> { let entity = self.entity_iter.next_back()?; // SAFETY: @@ -2024,7 +2077,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator> Iterator for QuerySortedManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -2185,7 +2238,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// . /// It is always safe for shared access. #[inline] - unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w>; K]> { + unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w, 's>; K]> { // PERF: can speed up the following code using `cursor.remaining()` instead of `next_item.is_none()` // when D::IS_ARCHETYPAL && F::IS_ARCHETYPAL // @@ -2211,11 +2264,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< } } - let mut values = MaybeUninit::<[D::Item<'w>; K]>::uninit(); + let mut values = MaybeUninit::<[D::Item<'w, 's>; K]>::uninit(); - let ptr = values.as_mut_ptr().cast::>(); + let ptr = values.as_mut_ptr().cast::>(); for (offset, cursor) in self.cursors.iter_mut().enumerate() { - ptr.add(offset).write(cursor.peek_last().unwrap()); + ptr.add(offset) + .write(cursor.peek_last(self.query_state).unwrap()); } Some(values.assume_init()) @@ -2223,7 +2277,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// Get next combination of queried components #[inline] - pub fn fetch_next(&mut self) -> Option<[D::Item<'_>; K]> { + pub fn fetch_next(&mut self) -> Option<[D::Item<'_, 's>; K]> { // SAFETY: we are limiting the returned reference to self, // making sure this method cannot be called multiple times without getting rid // of any previously returned unique references first, thus preventing aliasing. @@ -2240,7 +2294,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> Iterator for QueryCombinationIter<'w, 's, D, F, K> { - type Item = [D::Item<'w>; K]; + type Item = [D::Item<'w, 's>; K]; #[inline] fn next(&mut self) -> Option { @@ -2390,7 +2444,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// The result of `next` and any previous calls to `peek_last` with this row must have been /// dropped to prevent aliasing mutable references. #[inline] - unsafe fn peek_last(&mut self) -> Option> { + unsafe fn peek_last(&mut self, query_state: &'s QueryState) -> Option> { if self.current_row > 0 { let index = self.current_row - 1; if self.is_dense { @@ -2401,6 +2455,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `*entity` and `index` are in the current table. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, *entity, // SAFETY: This is from an exclusive range, so it can't be max. @@ -2416,6 +2471,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `archetype_entity.id()` and `archetype_entity.table_row()` are in the current archetype. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -2457,7 +2513,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { tables: &'w Tables, archetypes: &'w Archetypes, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { if self.is_dense { loop { // we are on the beginning of the query, or finished processing a table, so skip to the next @@ -2484,7 +2540,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { unsafe { self.table_entities.get_unchecked(self.current_row as usize) }; // SAFETY: The row is less than the u32 len, so it must not be max. let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(self.current_row)) }; - if !F::filter_fetch(&mut self.filter, *entity, row) { + if !F::filter_fetch(&query_state.filter_state, &mut self.filter, *entity, row) { self.current_row += 1; continue; } @@ -2494,7 +2550,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `current_row` must be a table row in range of the current table, // because if it was not, then the above would have been executed. // - fetch is only called once for each `entity`. - let item = unsafe { D::fetch(&mut self.fetch, *entity, row) }; + let item = + unsafe { D::fetch(&query_state.fetch_state, &mut self.fetch, *entity, row) }; self.current_row += 1; return Some(item); @@ -2536,6 +2593,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { .get_unchecked(self.current_row as usize) }; if !F::filter_fetch( + &query_state.filter_state, &mut self.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -2551,6 +2609,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - fetch is only called once for each `archetype_entity`. let item = unsafe { D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index eb5e001e4d..0bd3bbed23 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -507,7 +507,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::query::tests::A conflicts with a previous access in this query."] + #[should_panic] fn self_conflicting_worldquery() { #[derive(QueryData)] #[query_data(mutable)] @@ -824,9 +824,9 @@ mod tests { fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -835,18 +835,18 @@ mod tests { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _table: &'w Table, ) { } @@ -882,16 +882,20 @@ mod tests { unsafe impl QueryData for ReadsRData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index c685659260..b8d8618fa5 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -39,7 +39,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -76,7 +76,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -190,7 +190,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -247,7 +247,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -345,7 +345,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -402,7 +402,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 836fefbdfd..00d8b6f970 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -14,6 +14,7 @@ use crate::{ use crate::entity::UniqueEntityEquivalentSlice; use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use core::{fmt, ptr}; use fixedbitset::FixedBitSet; use log::warn; @@ -672,7 +673,7 @@ impl QueryState { assert!( component_access.is_subset(&self_access), "Transmuted state for {} attempts to access terms that are not allowed by original state {}.", - core::any::type_name::<(NewD, NewF)>(), core::any::type_name::<(D, F)>() + DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>() ); QueryState { @@ -791,7 +792,7 @@ impl QueryState { assert!( component_access.is_subset(&joined_component_access), "Joined state for {} attempts to access terms that are not allowed by state {} joined with {}.", - core::any::type_name::<(NewD, NewF)>(), core::any::type_name::<(D, F)>(), core::any::type_name::<(OtherD, OtherF)>() + DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>(), DebugName::type_name::<(OtherD, OtherF)>() ); if self.archetype_generation != other.archetype_generation { @@ -845,13 +846,17 @@ impl QueryState { /// /// This can only be called for read-only queries, see [`Self::get_mut`] for write-queries. /// + /// If you need to get multiple items at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::get_manual`] calls, + /// or making a single call with [`Self::get_many`] or [`Self::iter_many`]. + /// /// This is always guaranteed to run in `O(1)` time. #[inline] pub fn get<'w>( &mut self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query(world).get_inner(entity) } @@ -892,7 +897,7 @@ impl QueryState { &mut self, world: &'w World, entities: [Entity; N], - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_inner(entities) } @@ -930,7 +935,7 @@ impl QueryState { &mut self, world: &'w World, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_unique_inner(entities) } @@ -942,7 +947,7 @@ impl QueryState { &mut self, world: &'w mut World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_mut(world).get_inner(entity) } @@ -989,7 +994,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_mut_inner(entities) } @@ -1034,7 +1039,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_unique_inner(entities) } @@ -1056,7 +1061,7 @@ impl QueryState { &self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_manual(world).get_inner(entity) } @@ -1073,13 +1078,16 @@ impl QueryState { &mut self, world: UnsafeWorldCell<'w>, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_unchecked(world).get_inner(entity) } /// Returns an [`Iterator`] over the query results for the given [`World`]. /// /// This can only be called for read-only queries, see [`Self::iter_mut`] for write-queries. + /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_manual`] calls. #[inline] pub fn iter<'w, 's>(&'s mut self, world: &'w World) -> QueryIter<'w, 's, D::ReadOnly, F> { self.query(world).into_iter() @@ -1168,6 +1176,9 @@ impl QueryState { /// Items are returned in the order of the list of entities. /// Entities that don't match the query are skipped. /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_many_manual`] calls. + /// /// # See also /// /// - [`iter_many_mut`](Self::iter_many_mut) to get mutable query items. @@ -1387,8 +1398,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, T, FN, INIT>( - &self, + pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, 's, T, FN, INIT>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, batch_size: u32, @@ -1396,7 +1407,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: @@ -1501,8 +1512,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &UniqueEntityEquivalentSlice, @@ -1511,7 +1522,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1564,8 +1575,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &[E], @@ -1574,7 +1585,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1686,7 +1697,10 @@ impl QueryState { /// /// Simply unwrapping the [`Result`] also works, but should generally be reserved for tests. #[inline] - pub fn single<'w>(&mut self, world: &'w World) -> Result, QuerySingleError> { + pub fn single<'w>( + &mut self, + world: &'w World, + ) -> Result, QuerySingleError> { self.query(world).single_inner() } @@ -1703,7 +1717,7 @@ impl QueryState { pub fn single_mut<'w>( &mut self, world: &'w mut World, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_mut(world).single_inner() } @@ -1720,7 +1734,7 @@ impl QueryState { pub unsafe fn single_unchecked<'w>( &mut self, world: UnsafeWorldCell<'w>, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_unchecked(world).single_inner() } @@ -1742,7 +1756,7 @@ impl QueryState { world: UnsafeWorldCell<'w>, last_run: Tick, this_run: Tick, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { // SAFETY: // - The caller ensured we have the correct access to the world. // - The caller ensured that the world matches. @@ -1887,9 +1901,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for ((&bevy_ecs::query::state::tests::A, &bevy_ecs::query::state::tests::B), ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_to_include_data_not_in_original_query() { let mut world = World::new(); world.register_component::(); @@ -1901,9 +1913,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_immut_to_mut() { let mut world = World::new(); world.spawn(A(0)); @@ -1913,9 +1923,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (core::option::Option<&bevy_ecs::query::state::tests::A>, ())." - )] + #[should_panic] fn cannot_transmute_option_to_immut() { let mut world = World::new(); world.spawn(C(0)); @@ -1927,9 +1935,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (bevy_ecs::world::entity_ref::EntityRef, ())." - )] + #[should_panic] fn cannot_transmute_entity_ref() { let mut world = World::new(); world.register_component::(); @@ -1995,9 +2001,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_changed_without_access() { let mut world = World::new(); world.register_component::(); @@ -2007,9 +2011,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. @@ -2116,9 +2118,7 @@ mod tests { } #[test] - #[should_panic(expected = "Joined state for (&bevy_ecs::query::state::tests::C, ()) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, ()) joined with (&bevy_ecs::query::state::tests::B, ()).")] + #[should_panic] fn cannot_join_wrong_fetch() { let mut world = World::new(); world.register_component::(); @@ -2128,12 +2128,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, bevy_ecs::query::filter::Without) \ - joined with (&bevy_ecs::query::state::tests::B, bevy_ecs::query::filter::Without)." - )] + #[should_panic] fn cannot_join_wrong_filter() { let mut world = World::new(); let query_1 = QueryState::<&A, Without>::new(&mut world); @@ -2142,9 +2137,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for ((&mut bevy_ecs::query::state::tests::A, &mut bevy_ecs::query::state::tests::B), ()) attempts to access terms that are not allowed by state (&bevy_ecs::query::state::tests::A, ()) joined with (&mut bevy_ecs::query::state::tests::B, ())." - )] + #[should_panic] fn cannot_join_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index a6bcbf58bd..1c739927ac 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -42,7 +42,7 @@ use variadics_please::all_tuples; /// [`QueryFilter`]: crate::query::QueryFilter pub unsafe trait WorldQuery { /// Per archetype/table state retrieved by this [`WorldQuery`] to compute [`Self::Item`](crate::query::QueryData::Item) for each entity. - type Fetch<'a>: Clone; + type Fetch<'w>: Clone; /// State used to construct a [`Self::Fetch`](WorldQuery::Fetch). This will be cached inside [`QueryState`](crate::query::QueryState), /// so it is best to move as much data / computation here as possible to reduce the cost of @@ -62,9 +62,9 @@ pub unsafe trait WorldQuery { /// in to this function. /// - `world` must have the **right** to access any access registered in `update_component_access`. /// - There must not be simultaneous resource access conflicting with readonly resource access registered in [`WorldQuery::update_component_access`]. - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &Self::State, + state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w>; @@ -87,9 +87,9 @@ pub unsafe trait WorldQuery { /// - `archetype` and `tables` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `table` must correspond to `archetype`. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ); @@ -101,7 +101,11 @@ pub unsafe trait WorldQuery { /// /// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table); + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &'s Self::State, + table: &'w Table, + ); /// Adds any component accesses used by this [`WorldQuery`] to `access`. /// @@ -166,7 +170,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(unsafe { $name::init_fetch(world, $name, last_run, this_run) },)*) @@ -175,9 +179,9 @@ macro_rules! impl_tuple_world_query { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -188,7 +192,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($name,)*) = fetch; let ($($state,)*) = state; // SAFETY: The invariants are upheld by the caller. diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index ee02aff86e..133591c405 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -5,6 +5,7 @@ //! //! Same as [`super::component`], but for bundles. use alloc::boxed::Box; +use bevy_utils::prelude::DebugName; use core::any::{Any, TypeId}; use crate::{ @@ -172,7 +173,7 @@ impl FromType for Refl _ => panic!( "expected bundle `{}` to be named struct or tuple", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ), } } @@ -215,7 +216,7 @@ impl FromType for Refl _ => panic!( "expected bundle `{}` to be a named struct or tuple", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ), } } diff --git a/crates/bevy_ecs/src/reflect/component.rs b/crates/bevy_ecs/src/reflect/component.rs index 893e9b13fa..8a5c17179e 100644 --- a/crates/bevy_ecs/src/reflect/component.rs +++ b/crates/bevy_ecs/src/reflect/component.rs @@ -70,7 +70,7 @@ use crate::{ }, }; use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, TypePath, TypeRegistry}; -use disqualified::ShortName; +use bevy_utils::prelude::DebugName; /// A struct used to operate on reflected [`Component`] trait of a type. /// @@ -308,7 +308,8 @@ impl FromType for ReflectComponent { }, apply: |mut entity, reflected_component| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::apply` on component {name}. It is immutable, and cannot modified through reflection"); } @@ -357,7 +358,8 @@ impl FromType for ReflectComponent { reflect: |entity| entity.get::().map(|c| c as &dyn Reflect), reflect_mut: |entity| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::reflect_mut` on component {name}. It is immutable, and cannot modified through reflection"); } @@ -370,7 +372,8 @@ impl FromType for ReflectComponent { }, reflect_unchecked_mut: |entity| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::reflect_unchecked_mut` on component {name}. It is immutable, and cannot modified through reflection"); } diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index b630f58719..24e1449e61 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -18,6 +18,7 @@ mod from_world; mod map_entities; mod resource; +use bevy_utils::prelude::DebugName; pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; pub use entity_commands::ReflectCommandExt; @@ -136,7 +137,7 @@ pub fn from_reflect_with_fallback( `Default` or `FromWorld` traits. Are you perhaps missing a `#[reflect(Default)]` \ or `#[reflect(FromWorld)]`?", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ); }; diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 00334cb6d0..8830998663 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -6,6 +6,7 @@ mod relationship_source_collection; use alloc::format; +use bevy_utils::prelude::DebugName; pub use related_methods::*; pub use relationship_query::*; pub use relationship_source_collection::*; @@ -81,6 +82,20 @@ pub trait Relationship: Component + Sized { /// Creates this [`Relationship`] from the given `entity`. fn from(entity: Entity) -> Self; + /// Changes the current [`Entity`] ID of the entity containing the [`RelationshipTarget`] to another one. + /// + /// This is useful for updating the relationship without overwriting other fields stored in `Self`. + /// + /// # Warning + /// + /// This should generally not be called by user code, as modifying the related entity could invalidate the + /// relationship. If this method is used, then the hooks [`on_replace`](Relationship::on_replace) have to + /// run before and [`on_insert`](Relationship::on_insert) after it. + /// This happens automatically when this method is called with [`EntityWorldMut::modify_component`]. + /// + /// Prefer to use regular means of insertions when possible. + fn set_risky(&mut self, entity: Entity); + /// The `on_insert` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection. fn on_insert( mut world: DeferredWorld, @@ -105,8 +120,8 @@ pub trait Relationship: Component + Sized { warn!( "{}The {}({target_entity:?}) relationship on entity {entity:?} points to itself. The invalid {} relationship has been removed.", caller.map(|location|format!("{location}: ")).unwrap_or_default(), - core::any::type_name::(), - core::any::type_name::() + DebugName::type_name::(), + DebugName::type_name::() ); world.commands().entity(entity).remove::(); return; @@ -125,8 +140,8 @@ pub trait Relationship: Component + Sized { warn!( "{}The {}({target_entity:?}) relationship on entity {entity:?} relates to an entity that does not exist. The invalid {} relationship has been removed.", caller.map(|location|format!("{location}: ")).unwrap_or_default(), - core::any::type_name::(), - core::any::type_name::() + DebugName::type_name::(), + DebugName::type_name::() ); world.commands().entity(entity).remove::(); } diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index fc6d1f1183..8bae76a84e 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -6,7 +6,7 @@ use crate::{ Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget, }, system::{Commands, EntityCommands}, - world::{EntityWorldMut, World}, + world::{DeferredWorld, EntityWorldMut, World}, }; use bevy_platform::prelude::{Box, Vec}; use core::{marker::PhantomData, mem}; @@ -42,7 +42,12 @@ impl<'w> EntityWorldMut<'w> { let id = self.id(); self.world_scope(|world| { for related in related { - world.entity_mut(*related).insert(R::from(id)); + world + .entity_mut(*related) + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Run, + ); } }); self @@ -98,7 +103,12 @@ impl<'w> EntityWorldMut<'w> { .collection_mut_risky() .place(*related, index); } else { - world.entity_mut(*related).insert(R::from(id)); + world + .entity_mut(*related) + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Run, + ); world .get_mut::(id) .expect("hooks should have added relationship target") @@ -139,41 +149,46 @@ impl<'w> EntityWorldMut<'w> { return self; } - let Some(mut existing_relations) = self.get_mut::() else { + let Some(existing_relations) = self.get_mut::() else { return self.add_related::(related); }; - // We take the collection here so we can modify it without taking the component itself (this would create archetype move). + // We replace the component here with a dummy value so we can modify it without taking it (this would create archetype move). // SAFETY: We eventually return the correctly initialized collection into the target. - let mut existing_relations = mem::replace( - existing_relations.collection_mut_risky(), - Collection::::with_capacity(0), + let mut relations = mem::replace( + existing_relations.into_inner(), + ::RelationshipTarget::from_collection_risky( + Collection::::with_capacity(0), + ), ); + let collection = relations.collection_mut_risky(); + let mut potential_relations = EntityHashSet::from_iter(related.iter().copied()); let id = self.id(); self.world_scope(|world| { - for related in existing_relations.iter() { + for related in collection.iter() { if !potential_relations.remove(related) { world.entity_mut(related).remove::(); } } for related in potential_relations { - // SAFETY: We'll manually be adjusting the contents of the parent to fit the final state. + // SAFETY: We'll manually be adjusting the contents of the `RelationshipTarget` to fit the final state. world .entity_mut(related) - .insert_with_relationship_hook_mode(R::from(id), RelationshipHookMode::Skip); + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Skip, + ); } }); // SAFETY: The entities we're inserting will be the entities that were either already there or entities that we've just inserted. - existing_relations.clear(); - existing_relations.extend_from_iter(related.iter().copied()); - self.insert(R::RelationshipTarget::from_collection_risky( - existing_relations, - )); + collection.clear(); + collection.extend_from_iter(related.iter().copied()); + self.insert(relations); self } @@ -239,11 +254,20 @@ impl<'w> EntityWorldMut<'w> { assert_eq!(newly_related_entities, entities_to_relate, "`entities_to_relate` ({entities_to_relate:?}) didn't contain all entities that would end up related"); }; - if !self.contains::() { - self.add_related::(entities_to_relate); + match self.get_mut::() { + None => { + self.add_related::(entities_to_relate); - return self; - }; + return self; + } + Some(mut target) => { + // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. + let collection = target.collection_mut_risky(); + collection.clear(); + + collection.extend_from_iter(entities_to_relate.iter().copied()); + } + } let this = self.id(); self.world_scope(|world| { @@ -252,32 +276,16 @@ impl<'w> EntityWorldMut<'w> { } for new_relation in newly_related_entities { - // We're changing the target collection manually so don't run the insert hook + // We changed the target collection manually so don't run the insert hook world .entity_mut(*new_relation) - .insert_with_relationship_hook_mode(R::from(this), RelationshipHookMode::Skip); + .modify_or_insert_relation_with_relationship_hook_mode::( + this, + RelationshipHookMode::Skip, + ); } }); - if !entities_to_relate.is_empty() { - if let Some(mut target) = self.get_mut::() { - // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. - let collection = target.collection_mut_risky(); - collection.clear(); - - collection.extend_from_iter(entities_to_relate.iter().copied()); - } else { - let mut empty = - ::Collection::with_capacity( - entities_to_relate.len(), - ); - empty.extend_from_iter(entities_to_relate.iter().copied()); - - // SAFETY: We've just initialized this collection and we know there's no `RelationshipTarget` on `self` - self.insert(R::RelationshipTarget::from_collection_risky(empty)); - } - } - self } @@ -360,6 +368,40 @@ impl<'w> EntityWorldMut<'w> { self } + + fn modify_or_insert_relation_with_relationship_hook_mode( + &mut self, + entity: Entity, + relationship_hook_mode: RelationshipHookMode, + ) { + // Check if the relation edge holds additional data + if size_of::() > size_of::() { + self.assert_not_despawned(); + + let this = self.id(); + + let modified = self.world_scope(|world| { + let modified = DeferredWorld::from(&mut *world) + .modify_component_with_relationship_hook_mode::( + this, + relationship_hook_mode, + |r| r.set_risky(entity), + ) + .expect("entity access must be valid") + .is_some(); + + world.flush(); + + modified + }); + + if modified { + return; + } + } + + self.insert_with_relationship_hook_mode(R::from(entity), relationship_hook_mode); + } } impl<'a> EntityCommands<'a> { @@ -668,4 +710,176 @@ mod tests { assert_eq!(world.entity(b).get::(), None); assert_eq!(world.entity(c).get::(), None); } + + #[test] + fn replace_related_works() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut parent = world.spawn_empty(); + parent.add_children(&[child1, child2]); + let child_value = ChildOf(parent.id()); + let some_child = Some(&child_value); + + parent.replace_children(&[child2, child3]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child2, child3]); + assert_eq!(parent.world().get::(child1), None); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), some_child); + + parent.replace_children_with_difference(&[child3], &[child1, child2], &[child1]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child1, child2]); + assert_eq!(parent.world().get::(child1), some_child); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), None); + } + + #[test] + fn add_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world.entity_mut(parent2).add_related::(&[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + } + + #[test] + fn insert_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world + .entity_mut(parent2) + .insert_related::(0, &[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + } + + #[test] + fn replace_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world + .entity_mut(parent2) + .replace_related_with_difference::(&[], &[child], &[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + + world.entity_mut(parent1).replace_related::(&[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent1, + data: 42 + }) + ); + } + + #[test] + fn replace_related_keeps_relationship_target_data() { + #[derive(Component)] + #[relationship(relationship_target = Parent)] + struct Child(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent { + #[relationship] + children: Vec, + data: u8, + } + + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let mut parent = world.spawn_empty(); + parent.add_related::(&[child1]); + parent.get_mut::().unwrap().data = 42; + + parent.replace_related_with_difference::(&[child1], &[child2], &[child2]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + + parent.replace_related::(&[child1]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + } } diff --git a/crates/bevy_ecs/src/relationship/relationship_query.rs b/crates/bevy_ecs/src/relationship/relationship_query.rs index a2ec937c29..a7acea7de0 100644 --- a/crates/bevy_ecs/src/relationship/relationship_query.rs +++ b/crates/bevy_ecs/src/relationship/relationship_query.rs @@ -14,7 +14,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// target entity of that relationship. pub fn related(&'w self, entity: Entity) -> Option where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { self.get(entity).map(R::get).ok() } @@ -26,7 +26,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, { self.get(entity) .into_iter() @@ -42,7 +42,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity. pub fn root_ancestor(&'w self, entity: Entity) -> Entity where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { // Recursively search up the tree until we're out of parents match self.get(entity) { @@ -60,9 +60,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn iter_leaves( &'w self, entity: Entity, - ) -> impl Iterator + 'w + ) -> impl Iterator + use<'w, 's, S, D, F> where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { self.iter_descendants_depth_first(entity).filter(|entity| { @@ -80,7 +80,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, + D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, { self.get(entity) .ok() @@ -103,7 +103,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { DescendantIter::new(self, entity) } @@ -120,7 +120,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { DescendantDepthFirstIter::new(self, entity) @@ -137,7 +137,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { AncestorIter::new(self, entity) } @@ -148,7 +148,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Traverses the hierarchy breadth-first. pub struct DescendantIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, vecdeque: VecDeque, @@ -156,7 +156,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { /// Returns a new [`DescendantIter`]. pub fn new(children_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -174,7 +174,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { type Item = Entity; @@ -194,7 +194,7 @@ where /// Traverses the hierarchy depth-first. pub struct DescendantDepthFirstIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, stack: SmallVec<[Entity; 8]>, @@ -203,7 +203,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { /// Returns a new [`DescendantDepthFirstIter`]. @@ -220,7 +220,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { type Item = Entity; @@ -239,7 +239,7 @@ where /// An [`Iterator`] of [`Entity`]s over the ancestors of an [`Entity`]. pub struct AncestorIter<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { parent_query: &'w Query<'w, 's, D, F>, next: Option, @@ -247,7 +247,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { /// Returns a new [`AncestorIter`]. pub fn new(parent_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -261,7 +261,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> Iterator for AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { type Item = Entity; diff --git a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs index d4ea45f64f..01c9edf41b 100644 --- a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs +++ b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs @@ -86,13 +86,13 @@ pub trait OrderedRelationshipSourceCollection: RelationshipSourceCollection { /// Inserts the entity at a specific index. /// If the index is too large, the entity will be added to the end of the collection. fn insert(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. fn remove_at(&mut self, index: usize) -> Option; /// Inserts the entity at a specific index. /// This will never reorder other entities. /// If the index is too large, the entity will be added to the end of the collection. fn insert_stable(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. /// This will never reorder other entities. fn remove_at_stable(&mut self, index: usize) -> Option; /// Sorts the source collection. diff --git a/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs b/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs index dda6d604a7..997259e104 100644 --- a/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs +++ b/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs @@ -102,7 +102,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { let mut system_has_conditions_cache = HashMap::::default(); let mut is_valid_explicit_sync_point = |system: NodeId| { let index = system.index(); - is_apply_deferred(graph.systems[index].get().unwrap()) + is_apply_deferred(&graph.systems[index].get().unwrap().system) && !*system_has_conditions_cache .entry(index) .or_insert_with(|| system_has_conditions(graph, system)) @@ -138,7 +138,11 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { } else if !node_needs_sync { // No previous node has postponed sync points to add so check if the system itself // has deferred params that require a sync point to apply them. - node_needs_sync = graph.systems[node.index()].get().unwrap().has_deferred(); + node_needs_sync = graph.systems[node.index()] + .get() + .unwrap() + .system + .has_deferred(); } for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) { @@ -148,7 +152,11 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { let mut edge_needs_sync = node_needs_sync; if node_needs_sync - && !graph.systems[target.index()].get().unwrap().is_exclusive() + && !graph.systems[target.index()] + .get() + .unwrap() + .system + .is_exclusive() && self.no_sync_edges.contains(&(*node, target)) { // The node has deferred params to apply, but this edge is ignoring sync points. @@ -190,7 +198,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { continue; } - if is_apply_deferred(graph.systems[target.index()].get().unwrap()) { + if is_apply_deferred(&graph.systems[target.index()].get().unwrap().system) { // We don't need to insert a sync point since ApplyDeferred is a sync point // already! continue; diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 45eb4febb2..9a7ce5d507 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1,4 +1,5 @@ -use alloc::{borrow::Cow, boxed::Box, format}; +use alloc::{boxed::Box, format}; +use bevy_utils::prelude::DebugName; use core::ops::Not; use crate::system::{ @@ -154,7 +155,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(and); let name = format!("{} && {}", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `false` @@ -206,7 +207,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(nand); let name = format!("!({} && {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -258,7 +259,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(nor); let name = format!("!({} || {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that returns `true` @@ -305,7 +306,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(or); let name = format!("{} || {}", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -357,7 +358,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(xnor); let name = format!("!({} ^ {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -399,7 +400,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(xor); let name = format!("({} ^ {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } } @@ -466,7 +467,7 @@ pub mod common_conditions { use super::{NotSystem, SystemCondition}; use crate::{ change_detection::DetectChanges, - event::{Event, EventReader}, + event::{BufferedEvent, EventReader}, lifecycle::RemovedComponents, prelude::{Component, Query, With}, query::QueryFilter, @@ -928,7 +929,7 @@ pub mod common_conditions { /// my_system.run_if(on_event::), /// ); /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent; /// /// fn my_system(mut counter: ResMut) { @@ -945,7 +946,7 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn on_event(mut reader: EventReader) -> bool { + pub fn on_event(mut reader: EventReader) -> bool { // The 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), @@ -1328,6 +1329,7 @@ where #[cfg(test)] mod tests { use super::{common_conditions::*, SystemCondition}; + use crate::event::{BufferedEvent, Event}; use crate::query::With; use crate::{ change_detection::ResMut, @@ -1336,7 +1338,7 @@ mod tests { system::Local, world::World, }; - use bevy_ecs_macros::{Event, Resource}; + use bevy_ecs_macros::Resource; #[derive(Resource, Default)] struct Counter(usize); @@ -1447,7 +1449,7 @@ mod tests { #[derive(Component)] struct TestComponent; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent; #[derive(Resource)] diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 38b85c1ca5..9156fc34a3 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -3,7 +3,8 @@ mod multi_threaded; mod simple; mod single_threaded; -use alloc::{borrow::Cow, vec, vec::Vec}; +use alloc::{vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::any::TypeId; #[expect(deprecated, reason = "We still need to support this.")] @@ -15,11 +16,11 @@ pub use self::multi_threaded::{MainThreadExecutor, MultiThreadedExecutor}; use fixedbitset::FixedBitSet; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, query::FilteredAccessSet, - schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, + schedule::{ConditionWithAccess, InternedSystemSet, NodeId, SystemTypeSet, SystemWithAccess}, system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; @@ -74,9 +75,9 @@ pub struct SystemSchedule { /// List of system node ids. pub(super) system_ids: Vec, /// Indexed by system node id. - pub(super) systems: Vec, + pub(super) systems: Vec, /// Indexed by system node id. - pub(super) system_conditions: Vec>, + pub(super) system_conditions: Vec>, /// Indexed by system node id. /// Number of systems that the system immediately depends on. #[cfg_attr( @@ -97,7 +98,7 @@ pub struct SystemSchedule { /// List of system set node ids. pub(super) set_ids: Vec, /// Indexed by system set node id. - pub(super) set_conditions: Vec>, + pub(super) set_conditions: Vec>, /// Indexed by system set node id. /// List of systems that are in sets that have conditions. /// @@ -158,13 +159,8 @@ impl System for ApplyDeferred { type In = (); type Out = Result<()>; - fn name(&self) -> Cow<'static, str> { - Cow::Borrowed("bevy_ecs::apply_deferred") - } - - fn component_access_set(&self) -> &FilteredAccessSet { - // This system accesses no components. - const { &FilteredAccessSet::new() } + fn name(&self) -> DebugName { + DebugName::borrowed("bevy_ecs::apply_deferred") } fn flags(&self) -> SystemStateFlags { @@ -205,9 +201,11 @@ impl System for ApplyDeferred { Ok(()) } - fn initialize(&mut self, _world: &mut World) {} + fn initialize(&mut self, _world: &mut World) -> FilteredAccessSet { + FilteredAccessSet::new() + } - fn check_change_tick(&mut self, _change_tick: Tick) {} + fn check_change_tick(&mut self, _check: CheckChangeTicks) {} fn default_system_sets(&self) -> Vec { vec![SystemTypeSet::::new().intern()] diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 62a10298c9..7938a00e06 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -15,7 +15,10 @@ use tracing::{info_span, Span}; use crate::{ error::{ErrorContext, ErrorHandler, Result}, prelude::Resource, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + SystemWithAccess, + }, system::ScheduleSystem, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -27,14 +30,14 @@ use super::__rust_begin_short_backtrace; /// Borrowed data used by the [`MultiThreadedExecutor`]. struct Environment<'env, 'sys> { executor: &'env MultiThreadedExecutor, - systems: &'sys [SyncUnsafeCell], + systems: &'sys [SyncUnsafeCell], conditions: SyncUnsafeCell>, world_cell: UnsafeWorldCell<'env>, } struct Conditions<'a> { - system_conditions: &'a mut [Vec], - set_conditions: &'a mut [Vec], + system_conditions: &'a mut [Vec], + set_conditions: &'a mut [Vec], sets_with_conditions_of_systems: &'a [FixedBitSet], systems_in_sets_with_conditions: &'a [FixedBitSet], } @@ -172,8 +175,8 @@ impl SystemExecutor for MultiThreadedExecutor { conflicting_systems: FixedBitSet::with_capacity(sys_count), condition_conflicting_systems: FixedBitSet::with_capacity(sys_count), dependents: schedule.system_dependents[index].clone(), - is_send: schedule.systems[index].is_send(), - is_exclusive: schedule.systems[index].is_exclusive(), + is_send: schedule.systems[index].system.is_send(), + is_exclusive: schedule.systems[index].system.is_exclusive(), }); if schedule.system_dependencies[index] == 0 { self.starting_systems.insert(index); @@ -187,10 +190,7 @@ impl SystemExecutor for MultiThreadedExecutor { let system1 = &schedule.systems[index1]; for index2 in 0..index1 { let system2 = &schedule.systems[index2]; - if !system2 - .component_access_set() - .is_compatible(system1.component_access_set()) - { + if !system2.access.is_compatible(&system1.access) { state.system_task_metadata[index1] .conflicting_systems .insert(index2); @@ -202,11 +202,10 @@ impl SystemExecutor for MultiThreadedExecutor { for index2 in 0..sys_count { let system2 = &schedule.systems[index2]; - if schedule.system_conditions[index1].iter().any(|condition| { - !system2 - .component_access_set() - .is_compatible(condition.component_access_set()) - }) { + if schedule.system_conditions[index1] + .iter() + .any(|condition| !system2.access.is_compatible(&condition.access)) + { state.system_task_metadata[index1] .condition_conflicting_systems .insert(index2); @@ -220,11 +219,10 @@ impl SystemExecutor for MultiThreadedExecutor { let mut conflicting_systems = FixedBitSet::with_capacity(sys_count); for sys_index in 0..sys_count { let system = &schedule.systems[sys_index]; - if schedule.set_conditions[set_idx].iter().any(|condition| { - !system - .component_access_set() - .is_compatible(condition.component_access_set()) - }) { + if schedule.set_conditions[set_idx] + .iter() + .any(|condition| !system.access.is_compatible(&condition.access)) + { conflicting_systems.insert(sys_index); } } @@ -344,7 +342,7 @@ impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> { #[cfg(feature = "std")] #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); } // set the payload to propagate the error { @@ -355,6 +353,10 @@ impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> { self.tick_executor(); } + #[expect( + clippy::mut_from_ref, + reason = "Field is only accessed here and is guarded by lock with a documented safety comment" + )] fn try_lock<'a>(&'a self) -> Option<(&'a mut Conditions<'sys>, MutexGuard<'a, ExecutorState>)> { let guard = self.environment.executor.state.try_lock().ok()?; // SAFETY: This is an exclusive access as no other location fetches conditions mutably, and @@ -468,7 +470,8 @@ impl ExecutorState { debug_assert!(!self.running_systems.contains(system_index)); // SAFETY: Caller assured that these systems are not running. // Therefore, no other reference to this system exists and there is no aliasing. - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = + &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; #[cfg(feature = "hotpatching")] if should_update_hotpatch { @@ -661,7 +664,7 @@ impl ExecutorState { /// used by the specified system. unsafe fn spawn_system_task(&mut self, context: &Context, system_index: usize) { // SAFETY: this system is not running, no other reference exists - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; // Move the full context object into the new future. let context = *context; @@ -703,7 +706,7 @@ impl ExecutorState { /// Caller must ensure no systems are currently borrowed. unsafe fn spawn_exclusive_system_task(&mut self, context: &Context, system_index: usize) { // SAFETY: this system is not running, no other reference exists - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; // Move the full context object into the new future. let context = *context; @@ -785,12 +788,12 @@ impl ExecutorState { fn apply_deferred( unapplied_systems: &FixedBitSet, - systems: &[SyncUnsafeCell], + systems: &[SyncUnsafeCell], world: &mut World, ) -> Result<(), Box> { for system_index in unapplied_systems.ones() { // SAFETY: none of these systems are running, no other references exist - let system = unsafe { &mut *systems[system_index].get() }; + let system = &mut unsafe { &mut *systems[system_index].get() }.system; let res = std::panic::catch_unwind(AssertUnwindSafe(|| { system.apply_deferred(world); })); @@ -800,7 +803,7 @@ fn apply_deferred( { eprintln!( "Encountered a panic when applying buffers for system `{}`!", - &*system.name() + system.name() ); } return Err(payload); @@ -813,7 +816,7 @@ fn apply_deferred( /// - `world` must have permission to read any world data /// required by `conditions`. unsafe fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: UnsafeWorldCell, error_handler: ErrorHandler, ) -> bool { @@ -823,7 +826,7 @@ unsafe fn evaluate_and_fold_conditions( )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the condition. diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index d9069aa6e8..f0d655eab8 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -12,7 +12,8 @@ use std::eprintln; use crate::{ error::{ErrorContext, ErrorHandler}, schedule::{ - executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, + executor::is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, + SystemSchedule, }, world::World, }; @@ -70,9 +71,9 @@ impl SystemExecutor for SimpleExecutor { for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] - let name = schedule.systems[system_index].name(); + let name = schedule.systems[system_index].system.name(); #[cfg(feature = "trace")] - let should_run_span = info_span!("check_conditions", name = &*name).entered(); + let should_run_span = info_span!("check_conditions", name = name.as_string()).entered(); let mut should_run = !self.completed_systems.contains(system_index); for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() { @@ -105,7 +106,7 @@ impl SystemExecutor for SimpleExecutor { should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; if should_run { let valid_params = match system.validate_param(world) { Ok(()) => true, @@ -160,7 +161,7 @@ impl SystemExecutor for SimpleExecutor { #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); std::panic::resume_unwind(payload); } } @@ -195,7 +196,7 @@ impl SimpleExecutor { note = "Use SingleThreadedExecutor instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation." )] fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: &mut World, error_handler: ErrorHandler, ) -> bool { @@ -211,7 +212,7 @@ fn evaluate_and_fold_conditions( )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { match condition.validate_param(world) { Ok(()) => (), Err(e) => { diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 68af623b40..21b8d2289d 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -9,7 +9,9 @@ use std::eprintln; use crate::{ error::{ErrorContext, ErrorHandler}, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + }, world::World, }; #[cfg(feature = "hotpatching")] @@ -70,9 +72,9 @@ impl SystemExecutor for SingleThreadedExecutor { for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] - let name = schedule.systems[system_index].name(); + let name = schedule.systems[system_index].system.name(); #[cfg(feature = "trace")] - let should_run_span = info_span!("check_conditions", name = &*name).entered(); + let should_run_span = info_span!("check_conditions", name = name.as_string()).entered(); let mut should_run = !self.completed_systems.contains(system_index); for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() { @@ -105,7 +107,7 @@ impl SystemExecutor for SingleThreadedExecutor { should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; if should_run { let valid_params = match system.validate_param(world) { Ok(()) => true, @@ -164,7 +166,7 @@ impl SystemExecutor for SingleThreadedExecutor { #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); std::panic::resume_unwind(payload); } } @@ -204,7 +206,7 @@ impl SingleThreadedExecutor { fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) { for system_index in self.unapplied_systems.ones() { - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; system.apply_deferred(world); } @@ -213,7 +215,7 @@ impl SingleThreadedExecutor { } fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: &mut World, error_handler: ErrorHandler, ) -> bool { @@ -229,7 +231,7 @@ fn evaluate_and_fold_conditions( )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { match condition.validate_param(world) { Ok(()) => (), Err(e) => { diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 8c5aa1d6fb..12f58a7cd3 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -26,7 +26,9 @@ pub mod passes { #[cfg(test)] mod tests { use super::*; - use alloc::{string::ToString, vec, vec::Vec}; + #[cfg(feature = "trace")] + use alloc::string::ToString; + use alloc::{vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; use crate::error::BevyError; @@ -770,6 +772,7 @@ mod tests { } mod system_ambiguity { + #[cfg(feature = "trace")] use alloc::collections::BTreeSet; use super::*; @@ -784,8 +787,7 @@ mod tests { #[derive(Component)] struct B; - // An event type - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct E; #[derive(Resource, Component)] @@ -1111,6 +1113,7 @@ mod tests { // Tests that the correct ambiguities were reported in the correct order. #[test] + #[cfg(feature = "trace")] fn correct_ambiguities() { fn system_a(_res: ResMut) {} fn system_b(_res: ResMut) {} @@ -1184,6 +1187,7 @@ mod tests { // Test that anonymous set names work properly // Related issue https://github.com/bevyengine/bevy/issues/9641 #[test] + #[cfg(feature = "trace")] fn anonymous_set_name() { let mut schedule = Schedule::new(TestSchedule); schedule.add_systems((resmut_system, resmut_system).run_if(|| true)); diff --git a/crates/bevy_ecs/src/schedule/pass.rs b/crates/bevy_ecs/src/schedule/pass.rs index 20680e04e0..a602877d65 100644 --- a/crates/bevy_ecs/src/schedule/pass.rs +++ b/crates/bevy_ecs/src/schedule/pass.rs @@ -51,6 +51,7 @@ pub(super) trait ScheduleBuildPassObj: Send + Sync + Debug { ); fn add_dependency(&mut self, from: NodeId, to: NodeId, all_options: &TypeIdMap>); } + impl ScheduleBuildPassObj for T { fn build( &mut self, diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index c4e61356d0..4455aba2fe 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -2,7 +2,6 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17344." )] -use alloc::borrow::Cow; use alloc::{ boxed::Box, collections::{BTreeMap, BTreeSet}, @@ -12,12 +11,11 @@ use alloc::{ vec::Vec, }; use bevy_platform::collections::{HashMap, HashSet}; -use bevy_utils::{default, TypeIdMap}; +use bevy_utils::{default, prelude::DebugName, TypeIdMap}; use core::{ any::{Any, TypeId}, fmt::{Debug, Write}, }; -use disqualified::ShortName; use fixedbitset::FixedBitSet; use log::{error, info, warn}; use pass::ScheduleBuildPassObj; @@ -25,9 +23,11 @@ use thiserror::Error; #[cfg(feature = "trace")] use tracing::info_span; +use crate::component::CheckChangeTicks; use crate::{ - component::{ComponentId, Components, Tick}, + component::{ComponentId, Components}, prelude::Component, + query::FilteredAccessSet, resource::Resource, schedule::*, system::ScheduleSystem, @@ -111,7 +111,7 @@ impl Schedules { /// Iterates the change ticks of all systems in all stored schedules and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { #[cfg(feature = "trace")] let _all_span = info_span!("check stored schedule ticks").entered(); #[cfg_attr( @@ -126,7 +126,7 @@ impl Schedules { let name = format!("{label:?}"); #[cfg(feature = "trace")] let _one_span = info_span!("check schedule ticks", name = &name).entered(); - schedule.check_change_ticks(change_tick); + schedule.check_change_ticks(check); } } @@ -235,6 +235,7 @@ pub enum Chain { /// will be added between the successive elements. Chained(TypeIdMap>), } + impl Chain { /// Specify that the systems must be chained. pub fn set_chained(&mut self) { @@ -558,22 +559,22 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub fn check_change_ticks(&mut self, change_tick: Tick) { - for system in &mut self.executable.systems { + pub fn check_change_ticks(&mut self, check: CheckChangeTicks) { + for SystemWithAccess { system, .. } in &mut self.executable.systems { if !is_apply_deferred(system) { - system.check_change_tick(change_tick); + system.check_change_tick(check); } } for conditions in &mut self.executable.system_conditions { for system in conditions { - system.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } for conditions in &mut self.executable.set_conditions { for system in conditions { - system.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } } @@ -587,7 +588,7 @@ impl Schedule { /// This is used in rendering to extract data from the main world, storing the data in system buffers, /// before applying their buffers in a different world. pub fn apply_deferred(&mut self, world: &mut World) { - for system in &mut self.executable.systems { + for SystemWithAccess { system, .. } in &mut self.executable.systems { system.apply_deferred(world); } } @@ -609,7 +610,7 @@ impl Schedule { .system_ids .iter() .zip(&self.executable.systems) - .map(|(node_id, system)| (*node_id, system)); + .map(|(node_id, system)| (*node_id, &system.system)); Ok(iter) } @@ -677,26 +678,66 @@ impl SystemSetNode { } } -/// A [`ScheduleSystem`] stored in a [`ScheduleGraph`]. +/// A [`SystemWithAccess`] stored in a [`ScheduleGraph`]. pub struct SystemNode { - inner: Option, + inner: Option, +} + +/// A [`ScheduleSystem`] stored alongside the access returned from [`System::initialize`](crate::system::System::initialize). +pub struct SystemWithAccess { + /// The system itself. + pub system: ScheduleSystem, + /// The access returned by [`System::initialize`](crate::system::System::initialize). + /// This will be empty if the system has not been initialized yet. + pub access: FilteredAccessSet, +} + +impl SystemWithAccess { + /// Constructs a new [`SystemWithAccess`] from a [`ScheduleSystem`]. + /// The `access` will initially be empty. + pub fn new(system: ScheduleSystem) -> Self { + Self { + system, + access: FilteredAccessSet::new(), + } + } +} + +/// A [`BoxedCondition`] stored alongside the access returned from [`System::initialize`](crate::system::System::initialize). +pub struct ConditionWithAccess { + /// The condition itself. + pub condition: BoxedCondition, + /// The access returned by [`System::initialize`](crate::system::System::initialize). + /// This will be empty if the system has not been initialized yet. + pub access: FilteredAccessSet, +} + +impl ConditionWithAccess { + /// Constructs a new [`ConditionWithAccess`] from a [`BoxedCondition`]. + /// The `access` will initially be empty. + pub const fn new(condition: BoxedCondition) -> Self { + Self { + condition, + access: FilteredAccessSet::new(), + } + } } impl SystemNode { /// Create a new [`SystemNode`] pub fn new(system: ScheduleSystem) -> Self { Self { - inner: Some(system), + inner: Some(SystemWithAccess::new(system)), } } - /// Obtain a reference to the [`ScheduleSystem`] represented by this node. - pub fn get(&self) -> Option<&ScheduleSystem> { + /// Obtain a reference to the [`SystemWithAccess`] represented by this node. + pub fn get(&self) -> Option<&SystemWithAccess> { self.inner.as_ref() } - /// Obtain a mutable reference to the [`ScheduleSystem`] represented by this node. - pub fn get_mut(&mut self) -> Option<&mut ScheduleSystem> { + /// Obtain a mutable reference to the [`SystemWithAccess`] represented by this node. + pub fn get_mut(&mut self) -> Option<&mut SystemWithAccess> { self.inner.as_mut() } } @@ -710,11 +751,11 @@ pub struct ScheduleGraph { /// List of systems in the schedule pub systems: Vec, /// List of conditions for each system, in the same order as `systems` - pub system_conditions: Vec>, + pub system_conditions: Vec>, /// List of system sets in the schedule system_sets: Vec, /// List of conditions for each system set, in the same order as `system_sets` - system_set_conditions: Vec>, + system_set_conditions: Vec>, /// Map from system set to node id system_set_ids: HashMap, /// Systems that have not been initialized yet; for system sets, we store the index of the first uninitialized condition @@ -765,6 +806,7 @@ impl ScheduleGraph { self.systems .get(id.index()) .and_then(|system| system.inner.as_ref()) + .map(|system| &system.system) } /// Returns `true` if the given system set is part of the graph. Otherwise, returns `false`. @@ -801,7 +843,7 @@ impl ScheduleGraph { } /// Returns the conditions for the set at the given [`NodeId`], if it exists. - pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[BoxedCondition]> { + pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[ConditionWithAccess]> { if !id.is_set() { return None; } @@ -814,27 +856,31 @@ impl ScheduleGraph { /// /// Panics if it doesn't exist. #[track_caller] - pub fn set_conditions_at(&self, id: NodeId) -> &[BoxedCondition] { + pub fn set_conditions_at(&self, id: NodeId) -> &[ConditionWithAccess] { self.get_set_conditions_at(id) .ok_or_else(|| format!("set with id {id:?} does not exist in this Schedule")) .unwrap() } /// Returns an iterator over all systems in this schedule, along with the conditions for each system. - pub fn systems(&self) -> impl Iterator { + pub fn systems( + &self, + ) -> impl Iterator { self.systems .iter() .zip(self.system_conditions.iter()) .enumerate() .filter_map(|(i, (system_node, condition))| { - let system = system_node.inner.as_ref()?; + let system = &system_node.inner.as_ref()?.system; Some((NodeId::System(i), system, condition.as_slice())) }) } /// Returns an iterator over all system sets in this schedule, along with the conditions for each /// system set. - pub fn system_sets(&self) -> impl Iterator { + pub fn system_sets( + &self, + ) -> impl Iterator { self.system_set_ids.iter().map(|(_, &node_id)| { let set_node = &self.system_sets[node_id.index()]; let set = &*set_node.inner; @@ -1013,7 +1059,13 @@ impl ScheduleGraph { // system init has to be deferred (need `&mut World`) self.uninit.push((id, 0)); self.systems.push(SystemNode::new(config.node)); - self.system_conditions.push(config.conditions); + self.system_conditions.push( + config + .conditions + .into_iter() + .map(ConditionWithAccess::new) + .collect(), + ); Ok(id) } @@ -1031,7 +1083,7 @@ impl ScheduleGraph { let ScheduleConfig { node: set, metadata, - mut conditions, + conditions, } = set; let id = match self.system_set_ids.get(&set) { @@ -1045,7 +1097,7 @@ impl ScheduleGraph { // system init has to be deferred (need `&mut World`) let system_set_conditions = &mut self.system_set_conditions[id.index()]; self.uninit.push((id, system_set_conditions.len())); - system_set_conditions.append(&mut conditions); + system_set_conditions.extend(conditions.into_iter().map(ConditionWithAccess::new)); Ok(id) } @@ -1197,14 +1249,15 @@ impl ScheduleGraph { for (id, i) in self.uninit.drain(..) { match id { NodeId::System(index) => { - self.systems[index].get_mut().unwrap().initialize(world); + let system = self.systems[index].get_mut().unwrap(); + system.access = system.system.initialize(world); for condition in &mut self.system_conditions[index] { - condition.initialize(world); + condition.access = condition.condition.initialize(world); } } NodeId::Set(index) => { for condition in self.system_set_conditions[index].iter_mut().skip(i) { - condition.initialize(world); + condition.access = condition.condition.initialize(world); } } } @@ -1415,26 +1468,28 @@ impl ScheduleGraph { let system_a = self.systems[a.index()].get().unwrap(); let system_b = self.systems[b.index()].get().unwrap(); - if system_a.is_exclusive() || system_b.is_exclusive() { + if system_a.system.is_exclusive() || system_b.system.is_exclusive() { conflicting_systems.push((a, b, Vec::new())); } else { - let access_a = system_a.component_access_set(); - let access_b = system_b.component_access_set(); - match access_a.get_conflicts(access_b) { - AccessConflicts::Individual(conflicts) => { - let conflicts: Vec<_> = conflicts - .ones() - .map(ComponentId::get_sparse_set_index) - .filter(|id| !ignored_ambiguities.contains(id)) - .collect(); - if !conflicts.is_empty() { - conflicting_systems.push((a, b, conflicts)); + let access_a = &system_a.access; + let access_b = &system_b.access; + if !access_a.is_compatible(access_b) { + match access_a.get_conflicts(access_b) { + AccessConflicts::Individual(conflicts) => { + let conflicts: Vec<_> = conflicts + .ones() + .map(ComponentId::get_sparse_set_index) + .filter(|id| !ignored_ambiguities.contains(id)) + .collect(); + if !conflicts.is_empty() { + conflicting_systems.push((a, b, conflicts)); + } + } + AccessConflicts::All => { + // there is no specific component conflicting, but the systems are overall incompatible + // for example 2 systems with `Query` + conflicting_systems.push((a, b, Vec::new())); } - } - AccessConflicts::All => { - // there is no specific component conflicting, but the systems are overall incompatible - // for example 2 systems with `Query` - conflicting_systems.push((a, b, Vec::new())); } } } @@ -1638,9 +1693,14 @@ impl ScheduleGraph { #[inline] fn get_node_name_inner(&self, id: &NodeId, report_sets: bool) -> String { - let name = match id { + match id { NodeId::System(_) => { - let name = self.systems[id.index()].get().unwrap().name().to_string(); + let name = self.systems[id.index()].get().unwrap().system.name(); + let name = if self.settings.use_shortnames { + name.shortname().to_string() + } else { + name.to_string() + }; if report_sets { let sets = self.names_of_sets_containing_node(id); if sets.is_empty() { @@ -1662,11 +1722,6 @@ impl ScheduleGraph { set.name() } } - }; - if self.settings.use_shortnames { - ShortName(&name).to_string() - } else { - name } } @@ -1946,7 +2001,7 @@ impl ScheduleGraph { &'a self, ambiguities: &'a [(NodeId, NodeId, Vec)], components: &'a Components, - ) -> impl Iterator>)> + 'a { + ) -> impl Iterator)> + 'a { ambiguities .iter() .map(move |(system_a, system_b, conflicts)| { diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 2243e5019f..da91f616e9 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -1,4 +1,5 @@ use alloc::boxed::Box; +use bevy_utils::prelude::DebugName; use core::{ any::TypeId, fmt::Debug, @@ -196,7 +197,7 @@ impl SystemTypeSet { impl Debug for SystemTypeSet { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("SystemTypeSet") - .field(&format_args!("fn {}()", &core::any::type_name::())) + .field(&format_args!("fn {}()", DebugName::type_name::())) .finish() } } diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index b6de7c8215..b0d8b57fb0 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -895,9 +895,9 @@ mod tests { ($schedule:expr, $skipped_systems:expr, $($system:expr),*) => { // pull an ordered list of systems in the schedule, and save the // system TypeId, and name. - let systems: Vec<(TypeId, alloc::borrow::Cow<'static, str>)> = $schedule.systems().unwrap() + let systems: Vec<(TypeId, alloc::string::String)> = $schedule.systems().unwrap() .map(|(_, system)| { - (system.type_id(), system.name()) + (system.type_id(), system.name().as_string()) }) .collect(); diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index d5014f2240..0c30c14b9c 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -210,6 +210,7 @@ unsafe impl + Send + Sync + 'static> Bundle ); } } + impl> DynamicBundle for SpawnRelatedBundle { type Effect = Self; diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index fa58610bdf..29752ae2e5 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,10 +1,10 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped, TicksMut}, - component::{ComponentId, ComponentTicks, Components, Tick, TickCells}, + component::{CheckChangeTicks, ComponentId, ComponentTicks, Components, Tick, TickCells}, storage::{blob_vec::BlobVec, SparseSet}, }; -use alloc::string::String; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, mem::ManuallyDrop, panic::Location}; #[cfg(feature = "std")] @@ -23,7 +23,7 @@ pub struct ResourceData { not(feature = "std"), expect(dead_code, reason = "currently only used with the std feature") )] - type_name: String, + type_name: DebugName, #[cfg(feature = "std")] origin_thread_id: Option, changed_by: MaybeLocation>>, @@ -298,9 +298,9 @@ impl ResourceData { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.added_ticks.get_mut().check_tick(change_tick); - self.changed_ticks.get_mut().check_tick(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.added_ticks.get_mut().check_tick(check); + self.changed_ticks.get_mut().check_tick(check); } } @@ -385,7 +385,7 @@ impl Resources { data: ManuallyDrop::new(data), added_ticks: UnsafeCell::new(Tick::new(0)), changed_ticks: UnsafeCell::new(Tick::new(0)), - type_name: String::from(component_info.name()), + type_name: component_info.name(), #[cfg(feature = "std")] origin_thread_id: None, changed_by: MaybeLocation::caller().map(UnsafeCell::new), @@ -393,9 +393,9 @@ impl Resources { }) } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for info in self.resources.values_mut() { - info.check_change_ticks(change_tick); + info.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 42adcd89dc..bb28f967af 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, entity::{Entity, EntityRow}, storage::{Column, TableRow}, }; @@ -360,8 +360,8 @@ impl ComponentSparseSet { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.dense.check_change_ticks(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.dense.check_change_ticks(check); } } @@ -650,9 +650,9 @@ impl SparseSets { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for set in self.sets.values_mut() { - set.check_change_ticks(change_tick); + set.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index 78fafe0a26..acf531d9b9 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -228,20 +228,20 @@ impl ThinColumn { /// # Safety /// `len` is the actual length of this column #[inline] - pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, change_tick: Tick) { + pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, check: CheckChangeTicks) { for i in 0..len { // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.added_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.changed_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); } } @@ -646,12 +646,12 @@ impl Column { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for component_ticks in &mut self.added_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } for component_ticks in &mut self.changed_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } } diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 5f09d4226f..be75c58f03 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, entity::Entity, query::DebugCheckedUnwrap, storage::{blob_vec::BlobVec, ImmutableSparseSet, SparseSet}, @@ -629,11 +629,11 @@ impl Table { } /// Call [`Tick::check_tick`] on all of the ticks in the [`Table`] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { let len = self.entity_count() as usize; for col in self.columns.values_mut() { // SAFETY: `len` is the actual length of the column - unsafe { col.check_change_ticks(len, change_tick) }; + unsafe { col.check_change_ticks(len, check) }; } } @@ -793,9 +793,9 @@ impl Tables { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for table in &mut self.tables { - table.check_change_ticks(change_tick); + table.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 6caa002deb..c655ed9407 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -1,4 +1,5 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use super::{IntoSystem, ReadOnlySystem, System, SystemParamValidationError}; use crate::{ @@ -101,7 +102,7 @@ where pub struct AdapterSystem { func: Func, system: S, - name: Cow<'static, str>, + name: DebugName, } impl AdapterSystem @@ -110,7 +111,7 @@ where S: System, { /// Creates a new [`System`] that uses `func` to adapt `system`, via the [`Adapt`] trait. - pub const fn new(func: Func, system: S, name: Cow<'static, str>) -> Self { + pub const fn new(func: Func, system: S, name: DebugName) -> Self { Self { func, system, name } } } @@ -123,16 +124,10 @@ where type In = Func::In; type Out = Func::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access_set( - &self, - ) -> &crate::query::FilteredAccessSet { - self.system.component_access_set() - } - #[inline] fn flags(&self) -> super::SystemStateFlags { self.system.flags() @@ -175,12 +170,15 @@ where unsafe { self.system.validate_param_unsafe(world) } } - fn initialize(&mut self, world: &mut crate::prelude::World) { - self.system.initialize(world); + fn initialize( + &mut self, + world: &mut crate::prelude::World, + ) -> crate::query::FilteredAccessSet { + self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: crate::component::Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: crate::component::CheckChangeTicks) { + self.system.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 6536c9cc1e..7aea731314 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -7,7 +7,7 @@ use crate::{ query::{QueryData, QueryFilter, QueryState}, resource::Resource, system::{ - DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, + DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemParam, SystemParamValidationError, When, }, world::{ @@ -17,7 +17,7 @@ use crate::{ }; use core::fmt::Debug; -use super::{init_query_param, Res, ResMut, SystemState}; +use super::{Res, ResMut, SystemState}; /// A builder that can create a [`SystemParam`]. /// @@ -104,19 +104,15 @@ use super::{init_query_param, Res, ResMut, SystemState}; /// /// # Safety /// -/// The implementor must ensure the following is true. -/// - [`SystemParamBuilder::build`] correctly registers all [`World`] accesses used -/// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). -/// - None of the world accesses may conflict with any prior accesses registered -/// on `system_meta`. -/// -/// Note that this depends on the implementation of [`SystemParam::get_param`], +/// The implementor must ensure that the state returned +/// from [`SystemParamBuilder::build`] is valid for `P`. +/// Note that the exact safety requiremensts depend on the implementation of [`SystemParam`], /// so if `Self` is not a local type then you must call [`SystemParam::init_state`] -/// or another [`SystemParamBuilder::build`] +/// or another [`SystemParamBuilder::build`]. pub unsafe trait SystemParamBuilder: Sized { /// Registers any [`World`] access used by this [`SystemParam`] /// and creates a new instance of this param's [`State`](SystemParam::State). - fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State; + fn build(self, world: &mut World) -> P::State; /// Create a [`SystemState`] from a [`SystemParamBuilder`]. /// To create a system, call [`SystemState::build_system`] on the result. @@ -169,8 +165,8 @@ pub struct ParamBuilder; // SAFETY: Calls `SystemParam::init_state` unsafe impl SystemParamBuilder

for ParamBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State { - P::init_state(world, meta) + fn build(self, world: &mut World) -> P::State { + P::init_state(world) } } @@ -208,13 +204,13 @@ impl ParamBuilder { } } -// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +// SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, +// and we check the world during `build`. unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> SystemParamBuilder> for QueryState { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + fn build(self, world: &mut World) -> QueryState { self.validate_world(world.id()); - init_query_param(world, system_meta, &self); self } } @@ -282,7 +278,8 @@ impl<'a, D: QueryData, F: QueryFilter> } } -// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +// SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, +// and `QueryBuilder` produces one with the given `world`. unsafe impl< 'w, 's, @@ -291,12 +288,10 @@ unsafe impl< T: FnOnce(&mut QueryBuilder), > SystemParamBuilder> for QueryParamBuilder { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + fn build(self, world: &mut World) -> QueryState { let mut builder = QueryBuilder::new(world); (self.0)(&mut builder); - let state = builder.build(); - init_query_param(world, system_meta, &state); - state + builder.build() } } @@ -317,13 +312,13 @@ macro_rules! impl_system_param_builder_tuple { $(#[$meta])* // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls unsafe impl<$($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder<($($param,)*)> for ($($builder,)*) { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + fn build(self, world: &mut World) -> <($($param,)*) as SystemParam>::State { let ($($builder,)*) = self; #[allow( clippy::unused_unit, reason = "Zero-length tuples won't generate any calls to the system parameter builders." )] - ($($builder.build(world, meta),)*) + ($($builder.build(world),)*) } } }; @@ -340,9 +335,9 @@ all_tuples!( // SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls unsafe impl> SystemParamBuilder> for Vec { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { self.into_iter() - .map(|builder| builder.build(world, meta)) + .map(|builder| builder.build(world)) .collect() } } @@ -422,7 +417,7 @@ unsafe impl> SystemParamBuilder> pub struct ParamSetBuilder(pub T); macro_rules! impl_param_set_builder_tuple { - ($(($param: ident, $builder: ident, $meta: ident)),*) => { + ($(($param: ident, $builder: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is in a macro; as such, the below lints may not always apply." @@ -437,79 +432,38 @@ macro_rules! impl_param_set_builder_tuple { )] // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls unsafe impl<'w, 's, $($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder> for ParamSetBuilder<($($builder,)*)> { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + fn build(self, world: &mut World) -> <($($param,)*) as SystemParam>::State { let ParamSetBuilder(($($builder,)*)) = self; - // Note that this is slightly different from `init_state`, which calls `init_state` on each param twice. - // One call populates an empty `SystemMeta` with the new access, while the other runs against a cloned `SystemMeta` to check for conflicts. - // Builders can only be invoked once, so we do both in a single call here. - // That means that any `filtered_accesses` in the `component_access_set` will get copied to every `$meta` - // and will appear multiple times in the final `SystemMeta`. - $( - let mut $meta = system_meta.clone(); - let $param = $builder.build(world, &mut $meta); - )* - // Make the ParamSet non-send if any of its parameters are non-send. - if false $(|| !$meta.is_send())* { - system_meta.set_non_send(); - } - $( - system_meta - .component_access_set - .extend($meta.component_access_set); - )* - #[allow( - clippy::unused_unit, - reason = "Zero-length tuples won't generate any calls to the system parameter builders." - )] - ($($param,)*) + ($($builder.build(world),)*) } } }; } -all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta); +all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B); -// SAFETY: Relevant parameter ComponentId access is applied to SystemMeta. If any ParamState conflicts -// with any prior access, a panic will occur. +// SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls unsafe impl<'w, 's, P: SystemParam, B: SystemParamBuilder

> SystemParamBuilder>> for ParamSetBuilder> { - fn build( - self, - world: &mut World, - system_meta: &mut SystemMeta, - ) -> as SystemParam>::State { - let mut states = Vec::with_capacity(self.0.len()); - let mut metas = Vec::with_capacity(self.0.len()); - for builder in self.0 { - let mut meta = system_meta.clone(); - states.push(builder.build(world, &mut meta)); - metas.push(meta); - } - if metas.iter().any(|m| !m.is_send()) { - system_meta.set_non_send(); - } - for meta in metas { - system_meta - .component_access_set - .extend(meta.component_access_set); - } - states + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0 + .into_iter() + .map(|builder| builder.build(world)) + .collect() } } /// A [`SystemParamBuilder`] for a [`DynSystemParam`]. /// See the [`DynSystemParam`] docs for examples. -pub struct DynParamBuilder<'a>( - Box DynSystemParamState + 'a>, -); +pub struct DynParamBuilder<'a>(Box DynSystemParamState + 'a>); impl<'a> DynParamBuilder<'a> { /// Creates a new [`DynParamBuilder`] by wrapping a [`SystemParamBuilder`] of any type. /// The built [`DynSystemParam`] can be downcast to `T`. pub fn new(builder: impl SystemParamBuilder + 'a) -> Self { - Self(Box::new(|world, meta| { - DynSystemParamState::new::(builder.build(world, meta)) + Self(Box::new(|world| { + DynSystemParamState::new::(builder.build(world)) })) } } @@ -518,12 +472,8 @@ impl<'a> DynParamBuilder<'a> { // and the boxed builder was a valid implementation of `SystemParamBuilder` for that type. // The resulting `DynSystemParam` can only perform access by downcasting to that param type. unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamBuilder<'a> { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { - (self.0)(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + (self.0)(world) } } @@ -549,15 +499,11 @@ unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamB #[derive(Default, Debug, Clone)] pub struct LocalBuilder(pub T); -// SAFETY: `Local` performs no world access. +// SAFETY: Any value of `T` is a valid state for `Local`. unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder> for LocalBuilder { - fn build( - self, - _world: &mut World, - _meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, _world: &mut World) -> as SystemParam>::State { SyncCell::new(self.0) } } @@ -585,39 +531,14 @@ impl<'a> FilteredResourcesParamBuilder SystemParamBuilder> for FilteredResourcesParamBuilder { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { let mut builder = FilteredResourcesBuilder::new(world); (self.0)(&mut builder); - let access = builder.build(); - - let combined_access = meta.component_access_set.combined_access(); - let conflicts = combined_access.get_conflicts(&access); - if !conflicts.is_empty() { - let accesses = conflicts.format_conflict_list(world); - let system_name = &meta.name; - panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - } else { - for component_id in access.resource_reads_and_writes() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - } - } - - access + builder.build() } } @@ -644,49 +565,14 @@ impl<'a> FilteredResourcesMutParamBuilder SystemParamBuilder> for FilteredResourcesMutParamBuilder { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { let mut builder = FilteredResourcesMutBuilder::new(world); (self.0)(&mut builder); - let access = builder.build(); - - let combined_access = meta.component_access_set.combined_access(); - let conflicts = combined_access.get_conflicts(&access); - if !conflicts.is_empty() { - let accesses = conflicts.format_conflict_list(world); - let system_name = &meta.name; - panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - } else { - for component_id in access.resource_reads() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - } - } - - if access.has_write_all_resources() { - meta.component_access_set - .add_unfiltered_write_all_resources(); - } else { - for component_id in access.resource_writes() { - meta.component_access_set - .add_unfiltered_resource_write(component_id); - } - } - - access + builder.build() } } @@ -698,8 +584,8 @@ pub struct OptionBuilder(T); unsafe impl> SystemParamBuilder> for OptionBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { - self.0.build(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0.build(world) } } @@ -714,9 +600,8 @@ unsafe impl> fn build( self, world: &mut World, - meta: &mut SystemMeta, ) -> as SystemParam>::State { - self.0.build(world, meta) + self.0.build(world) } } @@ -728,8 +613,8 @@ pub struct WhenBuilder(T); unsafe impl> SystemParamBuilder> for WhenBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { - self.0.build(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0.build(world) } } diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 95fea44985..d48c599f2d 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -1,8 +1,9 @@ -use alloc::{borrow::Cow, format, vec::Vec}; +use alloc::{format, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::World, query::FilteredAccessSet, schedule::InternedSystemSet, @@ -55,7 +56,7 @@ use super::{IntoSystem, ReadOnlySystem, System}; /// IntoSystem::into_system(resource_equals(A(1))), /// IntoSystem::into_system(resource_equals(B(1))), /// // The name of the combined system. -/// std::borrow::Cow::Borrowed("a ^ b"), +/// "a ^ b".into(), /// ))); /// # fn my_system(mut flag: ResMut) { flag.0 = true; } /// # @@ -112,21 +113,19 @@ pub struct CombinatorSystem { _marker: PhantomData Func>, a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, + name: DebugName, } impl CombinatorSystem { /// Creates a new system that combines two inner systems. /// /// The returned system will only be usable if `Func` implements [`Combine`]. - pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { + pub fn new(a: A, b: B, name: DebugName) -> Self { Self { _marker: PhantomData, a, b, name, - component_access_set: FilteredAccessSet::default(), } } } @@ -140,14 +139,10 @@ where type In = Func::In; type Out = Func::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - #[inline] fn flags(&self) -> super::SystemStateFlags { self.a.flags() | self.b.flags() @@ -199,18 +194,16 @@ where unsafe { self.a.validate_param_unsafe(world) } } - fn initialize(&mut self, world: &mut World) { - self.a.initialize(world); - self.b.initialize(world); - self.component_access_set - .extend(self.a.component_access_set().clone()); - self.component_access_set - .extend(self.b.component_access_set().clone()); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + let mut a_access = self.a.initialize(world); + let b_access = self.b.initialize(world); + a_access.extend(b_access); + a_access } - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { @@ -279,7 +272,7 @@ where let system_a = IntoSystem::into_system(this.a); let system_b = IntoSystem::into_system(this.b); let name = format!("Pipe({}, {})", system_a.name(), system_b.name()); - PipeSystem::new(system_a, system_b, Cow::Owned(name)) + PipeSystem::new(system_a, system_b, DebugName::owned(name)) } } @@ -325,8 +318,7 @@ where pub struct PipeSystem { a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, + name: DebugName, } impl PipeSystem @@ -336,13 +328,8 @@ where for<'a> B::In: SystemInput = A::Out>, { /// Creates a new system that pipes two inner systems. - pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { - Self { - a, - b, - name, - component_access_set: FilteredAccessSet::default(), - } + pub fn new(a: A, b: B, name: DebugName) -> Self { + Self { a, b, name } } } @@ -355,14 +342,10 @@ where type In = A::In; type Out = B::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - #[inline] fn flags(&self) -> super::SystemStateFlags { self.a.flags() | self.b.flags() @@ -417,18 +400,16 @@ where Ok(()) } - fn initialize(&mut self, world: &mut World) { - self.a.initialize(world); - self.b.initialize(world); - self.component_access_set - .extend(self.a.component_access_set().clone()); - self.component_access_set - .extend(self.b.component_access_set().clone()); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + let mut a_access = self.a.initialize(world); + let b_access = self.b.initialize(world); + a_access.extend(b_access); + a_access } - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index af7b88edfc..5f1f611856 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -9,7 +9,7 @@ use crate::{ change_detection::MaybeLocation, entity::Entity, error::Result, - event::{Event, Events}, + event::{BufferedEvent, EntityEvent, Event, Events}, observer::TriggerTargets, resource::Resource, schedule::ScheduleLabel, @@ -144,10 +144,11 @@ where /// A [`Command`] that runs the given system, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached(system: S) -> impl Command +pub fn run_system_cached(system: S) -> impl Command where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached(system)?; @@ -157,11 +158,15 @@ where /// A [`Command`] that runs the given system with the given input value, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached_with(system: S, input: I::Inner<'static>) -> impl Command +pub fn run_system_cached_with( + system: S, + input: I::Inner<'static>, +) -> impl Command where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached_with(system, input)?; @@ -175,7 +180,7 @@ where pub fn unregister_system(system_id: SystemId) -> impl Command where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { move |world: &mut World| -> Result { world.unregister_system(system_id)?; @@ -208,7 +213,7 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { } } -/// A [`Command`] that sends a global [`Trigger`](crate::observer::Trigger) without any targets. +/// A [`Command`] that sends a global [`Event`] without any targets. #[track_caller] pub fn trigger(event: impl Event) -> impl Command { let caller = MaybeLocation::caller(); @@ -217,9 +222,10 @@ pub fn trigger(event: impl Event) -> impl Command { } } -/// A [`Command`] that sends a [`Trigger`](crate::observer::Trigger) for the given targets. +/// A [`Command`] that sends an [`EntityEvent`] for the given targets. +#[track_caller] pub fn trigger_targets( - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) -> impl Command { let caller = MaybeLocation::caller(); @@ -228,9 +234,9 @@ pub fn trigger_targets( } } -/// A [`Command`] that sends an arbitrary [`Event`]. +/// A [`Command`] that sends an arbitrary [`BufferedEvent`]. #[track_caller] -pub fn send_event(event: E) -> impl Command { +pub fn send_event(event: E) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { let mut events = world.resource_mut::>(); diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 317ad8476a..098493a148 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -11,8 +11,8 @@ use crate::{ bundle::{Bundle, InsertMode}, change_detection::MaybeLocation, component::{Component, ComponentId, ComponentInfo}, - entity::{Entity, EntityClonerBuilder}, - event::Event, + entity::{Entity, EntityClonerBuilder, OptIn, OptOut}, + event::EntityEvent, relationship::RelationshipHookMode, system::IntoObserverSystem, world::{error::EntityMutableFetchError, EntityWorldMut, FromWorld}, @@ -218,7 +218,7 @@ pub fn despawn() -> impl EntityCommand { /// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer) /// listening for events of type `E` targeting an entity #[track_caller] -pub fn observe( +pub fn observe( observer: impl IntoObserverSystem, ) -> impl EntityCommand { let caller = MaybeLocation::caller(); @@ -227,11 +227,11 @@ pub fn observe( } } -/// An [`EntityCommand`] that sends a [`Trigger`](crate::observer::Trigger) targeting an entity. +/// An [`EntityCommand`] that sends an [`EntityEvent`] targeting an entity. /// -/// This will run any [`Observer`](crate::observer::Observer) of the given [`Event`] watching the entity. +/// This will run any [`Observer`](crate::observer::Observer) of the given [`EntityEvent`] watching the entity. #[track_caller] -pub fn trigger(event: impl Event) -> impl EntityCommand { +pub fn trigger(event: impl EntityEvent) -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { let id = entity.id(); @@ -243,12 +243,36 @@ pub fn trigger(event: impl Event) -> impl EntityCommand { /// An [`EntityCommand`] that clones parts of an entity onto another entity, /// configured through [`EntityClonerBuilder`]. -pub fn clone_with( +/// +/// This builder tries to clone every component from the source entity except +/// for components that were explicitly denied, for example by using the +/// [`deny`](EntityClonerBuilder::deny) method. +/// +/// Required components are not considered by denied components and must be +/// explicitly denied as well if desired. +pub fn clone_with_opt_out( target: Entity, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> impl EntityCommand { move |mut entity: EntityWorldMut| { - entity.clone_with(target, config); + entity.clone_with_opt_out(target, config); + } +} + +/// An [`EntityCommand`] that clones parts of an entity onto another entity, +/// configured through [`EntityClonerBuilder`]. +/// +/// This builder tries to clone every component that was explicitly allowed +/// from the source entity, for example by using the +/// [`allow`](EntityClonerBuilder::allow) method. +/// +/// Required components are also cloned when the target entity does not contain them. +pub fn clone_with_opt_in( + target: Entity, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, +) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clone_with_opt_in(target, config); } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 3012a65458..0751e26770 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -18,9 +18,9 @@ use crate::{ bundle::{Bundle, InsertMode, NoBundleEffect}, change_detection::{MaybeLocation, Mut}, component::{Component, ComponentId, Mutable}, - entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, + entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut}, error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, - event::Event, + event::{BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, resource::Resource, schedule::ScheduleLabel, @@ -120,18 +120,28 @@ const _: () = { type Item<'w, 's> = Commands<'w, 's>; - fn init_state( - world: &mut World, - system_meta: &mut bevy_ecs::system::SystemMeta, - ) -> Self::State { + fn init_state(world: &mut World) -> Self::State { FetchState { state: <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::init_state( world, - system_meta, ), } } + fn init_access( + state: &Self::State, + system_meta: &mut bevy_ecs::system::SystemMeta, + component_access_set: &mut bevy_ecs::query::FilteredAccessSet, + world: &mut World, + ) { + <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); + } + fn apply( state: &mut Self::State, system_meta: &bevy_ecs::system::SystemMeta, @@ -862,7 +872,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// It will internally return a [`RegisteredSystemError`](crate::system::system_registry::RegisteredSystemError), /// which will be handled by [logging the error at the `warn` level](warn). - pub fn run_system(&mut self, id: SystemId) { + pub fn run_system(&mut self, id: SystemId<(), O>) { self.queue(command::run_system(id).handle_error_with(warn)); } @@ -955,7 +965,7 @@ impl<'w, 's> Commands<'w, 's> { ) -> SystemId where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { let entity = self.spawn_empty().id(); let system = RegisteredSystem::::new(Box::new(IntoSystem::into_system(system))); @@ -980,7 +990,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn unregister_system(&mut self, system_id: SystemId) where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { self.queue(command::unregister_system(system_id).handle_error_with(warn)); } @@ -1029,10 +1039,11 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs via [`Commands::run_system_cached_with`]. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached(&mut self, system: S) + pub fn run_system_cached(&mut self, system: S) where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { self.queue(command::run_system_cached(system).handle_error_with(warn)); } @@ -1059,16 +1070,17 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) + pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { self.queue(command::run_system_cached_with(system, input).handle_error_with(warn)); } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a global [`Event`] without any targets. /// /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. #[track_caller] @@ -1076,13 +1088,13 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::trigger(event)); } - /// Sends a [`Trigger`](crate::observer::Trigger) for the given targets. + /// Sends an [`EntityEvent`] for the given targets. /// - /// This will run any [`Observer`] of the given [`Event`] watching those targets. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. #[track_caller] pub fn trigger_targets( &mut self, - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.queue(command::trigger_targets(event, targets)); @@ -1091,7 +1103,7 @@ impl<'w, 's> Commands<'w, 's> { /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated /// with the entity that stores the observer. /// - /// `observer` can be any system whose first parameter is a [`Trigger`]. + /// `observer` can be any system whose first parameter is [`On`]. /// /// **Calling [`observe`](EntityCommands::observe) on the returned /// [`EntityCommands`] will observe the observer itself, which you very @@ -1101,7 +1113,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// Panics if the given system is an exclusive system. /// - /// [`Trigger`]: crate::observer::Trigger + /// [`On`]: crate::observer::On pub fn add_observer( &mut self, observer: impl IntoObserverSystem, @@ -1109,7 +1121,7 @@ impl<'w, 's> Commands<'w, 's> { self.spawn(Observer::new(observer)) } - /// Sends an arbitrary [`Event`]. + /// Sends an arbitrary [`BufferedEvent`]. /// /// This is a convenience method for sending events /// without requiring an [`EventWriter`](crate::event::EventWriter). @@ -1122,7 +1134,7 @@ impl<'w, 's> Commands<'w, 's> { /// If these events are performance-critical or very frequently sent, /// consider using a typed [`EventWriter`](crate::event::EventWriter) instead. #[track_caller] - pub fn send_event(&mut self, event: E) -> &mut Self { + pub fn send_event(&mut self, event: E) -> &mut Self { self.queue(command::send_event(event)); self } @@ -1947,16 +1959,16 @@ impl<'a> EntityCommands<'a> { &mut self.commands } - /// Sends a [`Trigger`](crate::observer::Trigger) targeting the entity. + /// Sends an [`EntityEvent`] targeting the entity. /// - /// This will run any [`Observer`] of the given [`Event`] watching this entity. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching this entity. #[track_caller] - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.queue(entity_command::trigger(event)) } /// Creates an [`Observer`] listening for events of type `E` targeting this entity. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { @@ -1966,8 +1978,9 @@ impl<'a> EntityCommands<'a> { /// Clones parts of an entity (components, observers, etc.) onto another entity, /// configured through [`EntityClonerBuilder`]. /// - /// By default, the other entity will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The other entity will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. /// /// # Panics /// @@ -1975,7 +1988,7 @@ impl<'a> EntityCommands<'a> { /// /// # Example /// - /// Configure through [`EntityClonerBuilder`] as follows: + /// Configure through [`EntityClonerBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; /// #[derive(Component, Clone)] @@ -1990,8 +2003,8 @@ impl<'a> EntityCommands<'a> { /// // Create a new entity and keep its EntityCommands. /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); /// - /// // Clone only ComponentA onto the target. - /// entity.clone_with(target, |builder| { + /// // Clone ComponentA but not ComponentB onto the target. + /// entity.clone_with_opt_out(target, |builder| { /// builder.deny::(); /// }); /// } @@ -1999,12 +2012,57 @@ impl<'a> EntityCommands<'a> { /// ``` /// /// See [`EntityClonerBuilder`] for more options. - pub fn clone_with( + pub fn clone_with_opt_out( &mut self, target: Entity, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> &mut Self { - self.queue(entity_command::clone_with(target, config)) + self.queue(entity_command::clone_with_opt_out(target, config)) + } + + /// Clones parts of an entity (components, observers, etc.) onto another entity, + /// configured through [`EntityClonerBuilder`]. + /// + /// The other entity will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// # Panics + /// + /// The command will panic when applied if the target entity does not exist. + /// + /// # Example + /// + /// Configure through [`EntityClonerBuilder`] as follows: + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component, Clone)] + /// struct ComponentA(u32); + /// #[derive(Component, Clone)] + /// struct ComponentB(u32); + /// + /// fn example_system(mut commands: Commands) { + /// // Create an empty entity. + /// let target = commands.spawn_empty().id(); + /// + /// // Create a new entity and keep its EntityCommands. + /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); + /// + /// // Clone ComponentA but not ComponentB onto the target. + /// entity.clone_with_opt_in(target, |builder| { + /// builder.allow::(); + /// }); + /// } + /// # bevy_ecs::system::assert_is_system(example_system); + /// ``` + /// + /// See [`EntityClonerBuilder`] for more options. + pub fn clone_with_opt_in( + &mut self, + target: Entity, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> &mut Self { + self.queue(entity_command::clone_with_opt_in(target, config)) } /// Spawns a clone of this entity and returns the [`EntityCommands`] of the clone. @@ -2013,7 +2071,8 @@ impl<'a> EntityCommands<'a> { /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). /// /// To configure cloning behavior (such as only cloning certain components), - /// use [`EntityCommands::clone_and_spawn_with`]. + /// use [`EntityCommands::clone_and_spawn_with_opt_out`]/ + /// [`opt_out`](EntityCommands::clone_and_spawn_with_opt_out). /// /// # Note /// @@ -2033,25 +2092,22 @@ impl<'a> EntityCommands<'a> { /// // Create a new entity and store its EntityCommands. /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); /// - /// // Create a clone of the first entity. + /// // Create a clone of the entity. /// let mut entity_clone = entity.clone_and_spawn(); /// } /// # bevy_ecs::system::assert_is_system(example_system); pub fn clone_and_spawn(&mut self) -> EntityCommands<'_> { - self.clone_and_spawn_with(|_| {}) + self.clone_and_spawn_with_opt_out(|_| {}) } /// Spawns a clone of this entity and allows configuring cloning behavior /// using [`EntityClonerBuilder`], returning the [`EntityCommands`] of the clone. /// - /// By default, the clone will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The clone will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. /// - /// To exclude specific components, use [`EntityClonerBuilder::deny`]. - /// To only include specific components, use [`EntityClonerBuilder::deny_all`] - /// followed by [`EntityClonerBuilder::allow`]. - /// - /// See the methods on [`EntityClonerBuilder`] for more options. + /// See the methods on [`EntityClonerBuilder`] for more options. /// /// # Note /// @@ -2071,18 +2127,63 @@ impl<'a> EntityCommands<'a> { /// // Create a new entity and store its EntityCommands. /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); /// - /// // Create a clone of the first entity, but without ComponentB. - /// let mut entity_clone = entity.clone_and_spawn_with(|builder| { + /// // Create a clone of the entity with ComponentA but without ComponentB. + /// let mut entity_clone = entity.clone_and_spawn_with_opt_out(|builder| { /// builder.deny::(); /// }); /// } /// # bevy_ecs::system::assert_is_system(example_system); - pub fn clone_and_spawn_with( + pub fn clone_and_spawn_with_opt_out( &mut self, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> EntityCommands<'_> { let entity_clone = self.commands().spawn_empty().id(); - self.clone_with(entity_clone, config); + self.clone_with_opt_out(entity_clone, config); + EntityCommands { + commands: self.commands_mut().reborrow(), + entity: entity_clone, + } + } + + /// Spawns a clone of this entity and allows configuring cloning behavior + /// using [`EntityClonerBuilder`], returning the [`EntityCommands`] of the clone. + /// + /// The clone will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// See the methods on [`EntityClonerBuilder`] for more options. + /// + /// # Note + /// + /// If the original entity does not exist when this command is applied, + /// the returned entity will have no components. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component, Clone)] + /// struct ComponentA(u32); + /// #[derive(Component, Clone)] + /// struct ComponentB(u32); + /// + /// fn example_system(mut commands: Commands) { + /// // Create a new entity and store its EntityCommands. + /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); + /// + /// // Create a clone of the entity with ComponentA but without ComponentB. + /// let mut entity_clone = entity.clone_and_spawn_with_opt_in(|builder| { + /// builder.allow::(); + /// }); + /// } + /// # bevy_ecs::system::assert_is_system(example_system); + pub fn clone_and_spawn_with_opt_in( + &mut self, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> EntityCommands<'_> { + let entity_clone = self.commands().spawn_empty().id(); + self.clone_with_opt_in(entity_clone, config); EntityCommands { commands: self.commands_mut().reborrow(), entity: entity_clone, @@ -2330,7 +2431,7 @@ mod tests { .spawn((W(1u32), W(2u64))) .id(); command_queue.apply(&mut world); - assert_eq!(world.entities().len(), 1); + assert_eq!(world.entity_count(), 1); let results = world .query::<(&W, &W)>() .iter(&world) diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 1cbdb5b07d..3a053f89d5 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,5 +1,5 @@ use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ @@ -10,6 +10,7 @@ use crate::{ }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; @@ -42,7 +43,7 @@ where /// /// Useful to give closure systems more readable and unique names for debugging and tracing. pub fn with_name(mut self, new_name: impl Into>) -> Self { - self.system_meta.set_name(new_name.into()); + self.system_meta.set_name(new_name); self } } @@ -83,15 +84,10 @@ where type Out = F::Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - #[inline] fn flags(&self) -> SystemStateFlags { // non-send , exclusive , no deferred @@ -175,17 +171,18 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); self.param_state = Some(F::Param::init(world, &mut self.system_meta)); + FilteredAccessSet::new() } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, - self.system_meta.name.as_ref(), + check, + self.system_meta.name.clone(), ); } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 7ad7f27e2b..6009662809 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,5 +1,5 @@ use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::FromWorld, query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, @@ -11,6 +11,7 @@ use crate::{ }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; @@ -24,11 +25,7 @@ use super::{ /// The metadata of a [`System`]. #[derive(Clone)] pub struct SystemMeta { - pub(crate) name: Cow<'static, str>, - /// The set of component accesses for this system. This is used to determine - /// - soundness issues (e.g. multiple [`SystemParam`]s mutably accessing the same component) - /// - ambiguities in the schedule (e.g. two systems that have some sort of conflicting access) - pub(crate) component_access_set: FilteredAccessSet, + pub(crate) name: DebugName, // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other flags: SystemStateFlags, @@ -41,22 +38,21 @@ pub struct SystemMeta { impl SystemMeta { pub(crate) fn new() -> Self { - let name = core::any::type_name::(); + let name = DebugName::type_name::(); Self { - name: name.into(), - component_access_set: FilteredAccessSet::default(), + #[cfg(feature = "trace")] + system_span: info_span!("system", name = name.clone().as_string()), + #[cfg(feature = "trace")] + commands_span: info_span!("system_commands", name = name.clone().as_string()), + name, flags: SystemStateFlags::empty(), last_run: Tick::new(0), - #[cfg(feature = "trace")] - system_span: info_span!("system", name = name), - #[cfg(feature = "trace")] - commands_span: info_span!("system_commands", name = name), } } /// Returns the system's name #[inline] - pub fn name(&self) -> &str { + pub fn name(&self) -> &DebugName { &self.name } @@ -72,7 +68,7 @@ impl SystemMeta { self.system_span = info_span!("system", name = name); self.commands_span = info_span!("system_commands", name = name); } - self.name = new_name; + self.name = new_name.into(); } /// Returns true if the system is [`Send`]. @@ -101,24 +97,6 @@ impl SystemMeta { pub fn set_has_deferred(&mut self) { self.flags |= SystemStateFlags::DEFERRED; } - - /// Returns a reference to the [`FilteredAccessSet`] for [`ComponentId`]. - /// Used to check if systems and/or system params have conflicting access. - #[inline] - pub fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - /// Returns a mutable reference to the [`FilteredAccessSet`] for [`ComponentId`]. - /// Used internally to statically check if systems have conflicting access. - /// - /// # Safety - /// - /// No access can be removed from the returned [`FilteredAccessSet`]. - #[inline] - pub unsafe fn component_access_set_mut(&mut self) -> &mut FilteredAccessSet { - &mut self.component_access_set - } } // TODO: Actually use this in FunctionSystem. We should probably only do this once Systems are constructed using a World reference @@ -154,7 +132,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # #[derive(Resource)] /// # struct MyResource(u32); @@ -187,7 +165,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// #[derive(Resource)] /// struct CachedSystemState { @@ -277,7 +255,11 @@ impl SystemState { pub fn new(world: &mut World) -> Self { let mut meta = SystemMeta::new::(); meta.last_run = world.change_tick().relative_to(Tick::MAX); - let param_state = Param::init_state(world, &mut meta); + let param_state = Param::init_state(world); + let mut component_access_set = FilteredAccessSet::new(); + // We need to call `init_access` to ensure there are no panics from conflicts within `Param`, + // even though we don't use the calculated access. + Param::init_access(¶m_state, &mut meta, &mut component_access_set, world); Self { meta, param_state, @@ -289,7 +271,11 @@ impl SystemState { pub(crate) fn from_builder(world: &mut World, builder: impl SystemParamBuilder) -> Self { let mut meta = SystemMeta::new::(); meta.last_run = world.change_tick().relative_to(Tick::MAX); - let param_state = builder.build(world, &mut meta); + let param_state = builder.build(world); + let mut component_access_set = FilteredAccessSet::new(); + // We need to call `init_access` to ensure there are no panics from conflicts within `Param`, + // even though we don't use the calculated access. + Param::init_access(¶m_state, &mut meta, &mut component_access_set, world); Self { meta, param_state, @@ -494,8 +480,7 @@ impl SystemState { /// Modifying the system param states may have unintended consequences. /// The param state is generally considered to be owned by the [`SystemParam`]. Modifications /// should respect any invariants as required by the [`SystemParam`]. - /// For example, modifying the system state of [`ResMut`](crate::system::ResMut) without also - /// updating [`SystemMeta::component_access_set`] will obviously create issues. + /// For example, modifying the system state of [`ResMut`](crate::system::ResMut) will obviously create issues. pub unsafe fn param_state_mut(&mut self) -> &mut Param::State { &mut self.param_state } @@ -616,15 +601,10 @@ where type Out = F::Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - #[inline] fn flags(&self) -> SystemStateFlags { self.system_meta.flags @@ -705,28 +685,35 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { if let Some(state) = &self.state { assert_eq!( state.world_id, world.id(), "System built with a different world than the one it was added to.", ); - } else { - self.state = Some(FunctionSystemState { - param: F::Param::init_state(world, &mut self.system_meta), - world_id: world.id(), - }); } + let state = self.state.get_or_insert_with(|| FunctionSystemState { + param: F::Param::init_state(world), + world_id: world.id(), + }); self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); + let mut component_access_set = FilteredAccessSet::new(); + F::Param::init_access( + &state.param, + &mut self.system_meta, + &mut component_access_set, + world, + ); + component_access_set } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, - self.system_meta.name.as_ref(), + check, + self.system_meta.name.clone(), ); } diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 12087fdf6a..cb75016ee9 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -2,7 +2,7 @@ use core::ops::{Deref, DerefMut}; use variadics_please::all_tuples; -use crate::{bundle::Bundle, prelude::Trigger, system::System}; +use crate::{bundle::Bundle, prelude::On, system::System}; /// Trait for types that can be used as input to [`System`]s. /// @@ -11,7 +11,7 @@ use crate::{bundle::Bundle, prelude::Trigger, system::System}; /// - [`In`]: For values /// - [`InRef`]: For read-only references to values /// - [`InMut`]: For mutable references to values -/// - [`Trigger`]: For [`ObserverSystem`]s +/// - [`On`]: For [`ObserverSystem`]s /// - [`StaticSystemInput`]: For arbitrary [`SystemInput`]s in generic contexts /// - Tuples of [`SystemInput`]s up to 8 elements /// @@ -222,9 +222,9 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// Used for [`ObserverSystem`]s. /// /// [`ObserverSystem`]: crate::system::ObserverSystem -impl SystemInput for Trigger<'_, E, B> { - type Param<'i> = Trigger<'i, E, B>; - type Inner<'i> = Trigger<'i, E, B>; +impl SystemInput for On<'_, E, B> { + type Param<'i> = On<'i, E, B>; + type Inner<'i> = On<'i, E, B>; fn wrap(this: Self::Inner<'_>) -> Self::Param<'_> { this diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index f79cb4f5d8..7a04e2fae0 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -410,7 +410,7 @@ mod tests { error::Result, lifecycle::RemovedComponents, name::Name, - prelude::{AnyOf, EntityRef, OnAdd, Trigger}, + prelude::{Add, AnyOf, EntityRef, On}, query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, resource::Resource, schedule::{ @@ -634,7 +634,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_ref() { fn sys(_: Query>) {} let mut world = World::default(); @@ -642,7 +642,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_ref_and_mut() { fn sys(_: Query>) {} let mut world = World::default(); @@ -650,7 +650,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_option() { fn sys(_: Query)>>) {} let mut world = World::default(); @@ -680,7 +680,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_conflicting() { fn sys(_: Query>) {} let mut world = World::default(); @@ -1163,12 +1163,10 @@ mod tests { let mut world = World::default(); let mut x = IntoSystem::into_system(sys_x); let mut y = IntoSystem::into_system(sys_y); - x.initialize(&mut world); - y.initialize(&mut world); + let x_access = x.initialize(&mut world); + let y_access = y.initialize(&mut world); - let conflicts = x - .component_access_set() - .get_conflicts(y.component_access_set()); + let conflicts = x_access.get_conflicts(&y_access); let b_id = world .components() .get_resource_id(TypeId::of::()) @@ -1631,54 +1629,42 @@ mod tests { } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_first() { fn system(_query: &World, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_second() { fn system(_: Query, _: &World) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_ref_and_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_first() { fn system(_world: DeferredWorld, _query: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "DeferredWorld in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_second::system conflicts with a previous access." - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_second() { fn system(_query: Query, _world: DeferredWorld) {} super::assert_system_does_not_conflict(system); @@ -1899,18 +1885,16 @@ mod tests { schedule.add_systems(sys); schedule.add_systems(|_query: Query<&Name>| {}); schedule.add_systems(|_query: Query<&Name>| todo!()); - #[expect(clippy::unused_unit, reason = "this forces the () return type")] schedule.add_systems(|_query: Query<&Name>| -> () { todo!() }); - fn obs(_trigger: Trigger) { + fn obs(_trigger: On) { todo!() } world.add_observer(obs); - world.add_observer(|_trigger: Trigger| {}); - world.add_observer(|_trigger: Trigger| todo!()); - #[expect(clippy::unused_unit, reason = "this forces the () return type")] - world.add_observer(|_trigger: Trigger| -> () { todo!() }); + world.add_observer(|_trigger: On| {}); + world.add_observer(|_trigger: On| todo!()); + world.add_observer(|_trigger: On| -> () { todo!() }); fn my_command(_world: &mut World) { todo!() @@ -1919,7 +1903,6 @@ mod tests { world.commands().queue(my_command); world.commands().queue(|_world: &mut World| {}); world.commands().queue(|_world: &mut World| todo!()); - #[expect(clippy::unused_unit, reason = "this forces the () return type")] world .commands() .queue(|_world: &mut World| -> () { todo!() }); diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 4891a39d45..243c2c3c3f 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -1,11 +1,12 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, never::Never, - prelude::{Bundle, Trigger}, + prelude::{Bundle, On}, query::FilteredAccessSet, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, @@ -14,14 +15,14 @@ use crate::{ use super::{IntoSystem, SystemParamValidationError}; -/// Implemented for [`System`]s that have a [`Trigger`] as the first argument. +/// Implemented for [`System`]s that have [`On`] as the first argument. pub trait ObserverSystem: - System, Out = Out> + Send + 'static + System, Out = Out> + Send + 'static { } impl ObserverSystem for T where - T: System, Out = Out> + Send + 'static + T: System, Out = Out> + Send + 'static { } @@ -35,7 +36,7 @@ impl ObserverSystem for T where #[diagnostic::on_unimplemented( message = "`{Self}` cannot become an `ObserverSystem`", label = "the trait `IntoObserverSystem` is not implemented", - note = "for function `ObserverSystem`s, ensure the first argument is a `Trigger` and any subsequent ones are `SystemParam`" + note = "for function `ObserverSystem`s, ensure the first argument is `On` and any subsequent ones are `SystemParam`" )] pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. @@ -47,7 +48,7 @@ pub trait IntoObserverSystem: Send + 'st impl IntoObserverSystem for S where - S: IntoSystem, Out, M> + Send + 'static, + S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, E: 'static, B: Bundle, @@ -61,7 +62,7 @@ where impl IntoObserverSystem for S where - S: IntoSystem, (), M> + Send + 'static, + S: IntoSystem, (), M> + Send + 'static, S::System: ObserverSystem, E: Send + Sync + 'static, B: Bundle, @@ -72,9 +73,10 @@ where InfallibleObserverWrapper::new(IntoSystem::into_system(this)) } } + impl IntoObserverSystem for S where - S: IntoSystem, Never, M> + Send + 'static, + S: IntoSystem, Never, M> + Send + 'static, E: Send + Sync + 'static, B: Bundle, { @@ -108,19 +110,14 @@ where B: Bundle, Out: Send + Sync + 'static, { - type In = Trigger<'static, E, B>; + type In = On<'static, E, B>; type Out = Result; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.observer.name() } - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.observer.component_access_set() - } - #[inline] fn flags(&self) -> super::SystemStateFlags { self.observer.flags() @@ -161,13 +158,13 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { - self.observer.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.observer.initialize(world) } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.observer.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.observer.check_change_tick(check); } #[inline] @@ -189,7 +186,7 @@ where mod tests { use crate::{ event::Event, - observer::Trigger, + observer::On, system::{In, IntoSystem}, world::World, }; @@ -199,7 +196,7 @@ mod tests { #[test] fn test_piped_observer_systems_no_input() { - fn a(_: Trigger) {} + fn a(_: On) {} fn b() {} let mut world = World::new(); @@ -208,7 +205,7 @@ mod tests { #[test] fn test_piped_observer_systems_with_inputs() { - fn a(_: Trigger) -> u32 { + fn a(_: On) -> u32 { 3 } fn b(_: In) {} diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index a8d6ecf8a8..6e44301b18 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1,3 +1,5 @@ +use bevy_utils::prelude::DebugName; + use crate::{ batching::BatchingStrategy, component::Tick, @@ -1185,7 +1187,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter_mut`]: Self::par_iter_mut /// [`World`]: crate::world::World #[inline] - pub fn par_iter(&self) -> QueryParIter<'_, '_, D::ReadOnly, F> { + pub fn par_iter(&self) -> QueryParIter<'_, 's, D::ReadOnly, F> { self.as_readonly().par_iter_inner() } @@ -1220,7 +1222,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter`]: Self::par_iter /// [`World`]: crate::world::World #[inline] - pub fn par_iter_mut(&mut self) -> QueryParIter<'_, '_, D, F> { + pub fn par_iter_mut(&mut self) -> QueryParIter<'_, 's, D, F> { self.reborrow().par_iter_inner() } @@ -1280,7 +1282,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many>( &self, entities: EntityList, - ) -> QueryParManyIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyIter { world: self.world, state: self.state.as_readonly(), @@ -1309,7 +1311,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique>( &self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state.as_readonly(), @@ -1338,7 +1340,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique_mut>( &mut self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state, @@ -1383,7 +1385,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get a mutable query item. #[inline] - pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { + pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { self.as_readonly().get_inner(entity) } @@ -1434,7 +1436,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many( &self, entities: [Entity; N], - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { // Note that we call a separate `*_inner` method from `get_many_mut` // because we don't need to check for duplicates. self.as_readonly().get_many_inner(entities) @@ -1485,7 +1487,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique( &self, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { self.as_readonly().get_many_unique_inner(entities) } @@ -1519,7 +1521,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get`](Self::get) to get a read-only query item. #[inline] - pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { self.reborrow().get_inner(entity) } @@ -1534,7 +1536,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get the item using a mutable borrow of the [`Query`]. #[inline] - pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { @@ -1580,8 +1582,18 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { D::set_archetype(&mut fetch, &self.state.fetch_state, archetype, table); F::set_archetype(&mut filter, &self.state.filter_state, archetype, table); - if F::filter_fetch(&mut filter, entity, location.table_row) { - Ok(D::fetch(&mut fetch, entity, location.table_row)) + if F::filter_fetch( + &self.state.filter_state, + &mut filter, + entity, + location.table_row, + ) { + Ok(D::fetch( + &self.state.fetch_state, + &mut fetch, + entity, + location.table_row, + )) } else { Err(QueryEntityError::QueryDoesNotMatch( entity, @@ -1662,7 +1674,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut( &mut self, entities: [Entity; N], - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_mut_inner(entities) } @@ -1730,7 +1742,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_mut( &mut self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_unique_inner(entities) } @@ -1749,7 +1761,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // Verify that all entities are unique for i in 0..N { for j in 0..i { @@ -1777,7 +1789,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> where D: ReadOnlyQueryData, { @@ -1799,7 +1811,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_inner( self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // SAFETY: All entities are unique, so the results don't alias. unsafe { self.get_many_impl(entities.into_inner()) } } @@ -1814,7 +1826,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { unsafe fn get_many_impl( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { let mut values = [(); N].map(|_| MaybeUninit::uninit()); for (value, entity) in core::iter::zip(&mut values, entities) { @@ -1842,7 +1854,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) for the safe version. #[inline] - pub unsafe fn get_unchecked(&self, entity: Entity) -> Result, QueryEntityError> { + pub unsafe fn get_unchecked( + &self, + entity: Entity, + ) -> Result, QueryEntityError> { // SAFETY: The caller promises that this will not result in multiple mutable references. unsafe { self.reborrow_unsafe() }.get_inner(entity) } @@ -1878,7 +1893,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single_mut`](Self::single_mut) to get the mutable query item. #[inline] - pub fn single(&self) -> Result, QuerySingleError> { + pub fn single(&self) -> Result, QuerySingleError> { self.as_readonly().single_inner() } @@ -1907,7 +1922,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single`](Self::single) to get the read-only query item. #[inline] - pub fn single_mut(&mut self) -> Result, QuerySingleError> { + pub fn single_mut(&mut self) -> Result, QuerySingleError> { self.reborrow().single_inner() } @@ -1939,15 +1954,15 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// - [`single_mut`](Self::single_mut) to get the mutable query item. /// - [`single_inner`](Self::single_inner) for the panicking version. #[inline] - pub fn single_inner(self) -> Result, QuerySingleError> { + pub fn single_inner(self) -> Result, QuerySingleError> { let mut query = self.into_iter(); let first = query.next(); let extra = query.next().is_some(); match (first, extra) { (Some(r), false) => Ok(r), - (None, _) => Err(QuerySingleError::NoEntities(core::any::type_name::())), - (Some(_), _) => Err(QuerySingleError::MultipleEntities(core::any::type_name::< + (None, _) => Err(QuerySingleError::NoEntities(DebugName::type_name::())), + (Some(_), _) => Err(QuerySingleError::MultipleEntities(DebugName::type_name::< Self, >())), } @@ -2451,7 +2466,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2464,7 +2479,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, F> { - type Item = ROQueryItem<'w, D>; + type Item = ROQueryItem<'w, 's, D>; type IntoIter = QueryIter<'w, 's, D::ReadOnly, F>; fn into_iter(self) -> Self::IntoIter { @@ -2473,7 +2488,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w mut Query<'_, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2573,28 +2588,43 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>> /// See [`Query`] for more details. /// /// [System parameter]: crate::system::SystemParam -pub struct Single<'w, D: QueryData, F: QueryFilter = ()> { - pub(crate) item: D::Item<'w>, +/// +/// # Example +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// struct Boss { +/// health: f32 +/// }; +/// +/// fn hurt_boss(mut boss: Single<&mut Boss>) { +/// boss.health -= 4.0; +/// } +/// ``` +/// Note that because [`Single`] implements [`Deref`] and [`DerefMut`], methods and fields like `health` can be accessed directly. +/// You can also access the underlying data manually, by calling `.deref`/`.deref_mut`, or by using the `*` operator. +pub struct Single<'w, 's, D: QueryData, F: QueryFilter = ()> { + pub(crate) item: D::Item<'w, 's>, pub(crate) _filter: PhantomData, } -impl<'w, D: QueryData, F: QueryFilter> Deref for Single<'w, D, F> { - type Target = D::Item<'w>; +impl<'w, 's, D: QueryData, F: QueryFilter> Deref for Single<'w, 's, D, F> { + type Target = D::Item<'w, 's>; fn deref(&self) -> &Self::Target { &self.item } } -impl<'w, D: QueryData, F: QueryFilter> DerefMut for Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> DerefMut for Single<'w, 's, D, F> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.item } } -impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> Single<'w, 's, D, F> { /// Returns the inner item with ownership. - pub fn into_inner(self) -> D::Item<'w> { + pub fn into_inner(self) -> D::Item<'w, 's> { self.item } } diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 5e05a5ada9..ca4bdd4460 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -1,7 +1,8 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, query::FilteredAccessSet, system::{input::SystemIn, BoxedSystem, System, SystemInput}, @@ -25,7 +26,7 @@ impl> System for InfallibleSystemWrapper { type Out = Result; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.0.name() } @@ -33,11 +34,6 @@ impl> System for InfallibleSystemWrapper { self.0.type_id() } - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.0.component_access_set() - } - #[inline] fn flags(&self) -> SystemStateFlags { self.0.flags() @@ -78,13 +74,13 @@ impl> System for InfallibleSystemWrapper { } #[inline] - fn initialize(&mut self, world: &mut World) { - self.0.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.0.initialize(world) } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.0.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.0.check_change_tick(check); } #[inline] @@ -142,17 +138,12 @@ where T: Send + Sync + 'static, { type In = (); - type Out = S::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system.name() } - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - #[inline] fn flags(&self) -> SystemStateFlags { self.system.flags() @@ -187,12 +178,12 @@ where self.system.validate_param_unsafe(world) } - fn initialize(&mut self, world: &mut World) { - self.system.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { @@ -240,17 +231,12 @@ where T: FromWorld + Send + Sync + 'static, { type In = (); - type Out = S::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system.name() } - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - #[inline] fn flags(&self) -> SystemStateFlags { self.system.flags() @@ -289,15 +275,15 @@ where self.system.validate_param_unsafe(world) } - fn initialize(&mut self, world: &mut World) { - self.system.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { if self.value.is_none() { self.value = Some(T::from_world(world)); } + self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index fc96e8a843..d4521e76f5 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -2,20 +2,21 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17353." )] +use bevy_utils::prelude::DebugName; use bitflags::bitflags; use core::fmt::Debug; use log::warn; use thiserror::Error; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; -use alloc::{borrow::Cow, boxed::Box, vec::Vec}; +use alloc::{boxed::Box, vec::Vec}; use core::any::TypeId; use super::{IntoSystem, SystemParamValidationError}; @@ -49,17 +50,15 @@ pub trait System: Send + Sync + 'static { type In: SystemInput; /// The system's output. type Out; + /// Returns the system's name. - fn name(&self) -> Cow<'static, str>; + fn name(&self) -> DebugName; /// Returns the [`TypeId`] of the underlying system type. #[inline] fn type_id(&self) -> TypeId { TypeId::of::() } - /// Returns the system's component [`FilteredAccessSet`]. - fn component_access_set(&self) -> &FilteredAccessSet; - /// Returns the [`SystemStateFlags`] of the system. fn flags(&self) -> SystemStateFlags; @@ -91,7 +90,7 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `component_access_set`. There must be no conflicting + /// registered in the access returned from [`System::initialize`]. There must be no conflicting /// simultaneous accesses while the system is running. /// - If [`System::is_exclusive`] returns `true`, then it must be valid to call /// [`UnsafeWorldCell::world_mut`] on `world`. @@ -152,7 +151,7 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `component_access_set`. There must be no conflicting + /// registered in the access returned from [`System::initialize`]. There must be no conflicting /// simultaneous accesses while the system is running. unsafe fn validate_param_unsafe( &mut self, @@ -169,13 +168,15 @@ pub trait System: Send + Sync + 'static { } /// Initialize the system. - fn initialize(&mut self, _world: &mut World); + /// + /// Returns a [`FilteredAccessSet`] with the access required to run the system. + fn initialize(&mut self, _world: &mut World) -> FilteredAccessSet; /// Checks any [`Tick`]s stored on this system and wraps their value if they get too old. /// /// This method must be called periodically to ensure that change detection behaves correctly. /// When using bevy's default configuration, this will be called for you as needed. - fn check_change_tick(&mut self, change_tick: Tick); + fn check_change_tick(&mut self, check: CheckChangeTicks); /// Returns the system's default [system sets](crate::schedule::SystemSet). /// @@ -225,9 +226,13 @@ pub unsafe trait ReadOnlySystem: System { /// A convenience type alias for a boxed [`System`] trait object. pub type BoxedSystem = Box>; -pub(crate) fn check_system_change_tick(last_run: &mut Tick, this_run: Tick, system_name: &str) { - if last_run.check_tick(this_run) { - let age = this_run.relative_to(*last_run).get(); +pub(crate) fn check_system_change_tick( + last_run: &mut Tick, + check: CheckChangeTicks, + system_name: DebugName, +) { + if last_run.check_tick(check) { + let age = check.present_tick().relative_to(*last_run).get(); warn!( "System '{system_name}' has not run for {age} ticks. \ Changes older than {} ticks will not be detected.", @@ -395,7 +400,7 @@ pub enum RunSystemError { #[error("System {system} did not run due to failed parameter validation: {err}")] InvalidParams { /// The identifier of the system that was run. - system: Cow<'static, str>, + system: DebugName, /// The returned parameter validation error. err: SystemParamValidationError, }, @@ -405,7 +410,6 @@ pub enum RunSystemError { mod tests { use super::*; use crate::prelude::*; - use alloc::string::ToString; #[test] fn run_system_once() { @@ -478,7 +482,5 @@ mod tests { let result = world.run_system_once(system); assert!(matches!(result, Err(RunSystemError::InvalidParams { .. }))); - let expected = "System bevy_ecs::system::system::tests::run_system_once_invalid_params::system did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."; - assert_eq!(expected, result.unwrap_err().to_string()); } } diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs index b28ddd89f6..e0c3c952cf 100644 --- a/crates/bevy_ecs/src/system/system_name.rs +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -1,12 +1,12 @@ use crate::{ - component::Tick, + component::{ComponentId, Tick}, prelude::World, + query::FilteredAccessSet, system::{ExclusiveSystemParam, ReadOnlySystemParam, SystemMeta, SystemParam}, world::unsafe_world_cell::UnsafeWorldCell, }; -use alloc::borrow::Cow; -use core::ops::Deref; -use derive_more::derive::{AsRef, Display, Into}; +use bevy_utils::prelude::DebugName; +use derive_more::derive::{Display, Into}; /// [`SystemParam`] that returns the name of the system which it is used in. /// @@ -19,11 +19,11 @@ use derive_more::derive::{AsRef, Display, Into}; /// # use bevy_ecs::system::SystemParam; /// /// #[derive(SystemParam)] -/// struct Logger<'s> { -/// system_name: SystemName<'s>, +/// struct Logger { +/// system_name: SystemName, /// } /// -/// impl<'s> Logger<'s> { +/// impl Logger { /// fn log(&mut self, message: &str) { /// eprintln!("{}: {}", self.system_name, message); /// } @@ -34,61 +34,58 @@ use derive_more::derive::{AsRef, Display, Into}; /// logger.log("Hello"); /// } /// ``` -#[derive(Debug, Into, Display, AsRef)] -#[as_ref(str)] -pub struct SystemName<'s>(&'s str); +#[derive(Debug, Into, Display)] +pub struct SystemName(DebugName); -impl<'s> SystemName<'s> { +impl SystemName { /// Gets the name of the system. - pub fn name(&self) -> &str { - self.0 - } -} - -impl<'s> Deref for SystemName<'s> { - type Target = str; - fn deref(&self) -> &Self::Target { - self.name() + pub fn name(&self) -> DebugName { + self.0.clone() } } // SAFETY: no component value access -unsafe impl SystemParam for SystemName<'_> { - type State = Cow<'static, str>; - type Item<'w, 's> = SystemName<'s>; +unsafe impl SystemParam for SystemName { + type State = (); + type Item<'w, 's> = SystemName; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.name.clone() + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { } #[inline] unsafe fn get_param<'w, 's>( - name: &'s mut Self::State, - _system_meta: &SystemMeta, + _state: &'s mut Self::State, + system_meta: &SystemMeta, _world: UnsafeWorldCell<'w>, _change_tick: Tick, ) -> Self::Item<'w, 's> { - SystemName(name) + SystemName(system_meta.name.clone()) } } // SAFETY: Only reads internal system state -unsafe impl<'s> ReadOnlySystemParam for SystemName<'s> {} +unsafe impl ReadOnlySystemParam for SystemName {} -impl ExclusiveSystemParam for SystemName<'_> { - type State = Cow<'static, str>; - type Item<'s> = SystemName<'s>; +impl ExclusiveSystemParam for SystemName { + type State = (); + type Item<'s> = SystemName; - fn init(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.name.clone() - } + fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} - fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { - SystemName(state) + fn get_param<'s>(_state: &'s mut Self::State, system_meta: &SystemMeta) -> Self::Item<'s> { + SystemName(system_meta.name.clone()) } } #[cfg(test)] +#[cfg(feature = "trace")] mod tests { use crate::{ system::{IntoSystem, RunSystemOnce, SystemName}, @@ -99,7 +96,7 @@ mod tests { #[test] fn test_system_name_regular_param() { fn testing(name: SystemName) -> String { - name.name().to_owned() + name.name().as_string() } let mut world = World::default(); @@ -111,7 +108,7 @@ mod tests { #[test] fn test_system_name_exclusive_param() { fn testing(_world: &mut World, name: SystemName) -> String { - name.name().to_owned() + name.name().as_string() } let mut world = World::default(); @@ -125,7 +122,7 @@ mod tests { let mut world = World::default(); let system = IntoSystem::into_system(|name: SystemName| name.name().to_owned()).with_name("testing"); - let name = world.run_system_once(system).unwrap(); + let name = world.run_system_once(system).unwrap().as_string(); assert_eq!(name, "testing"); } @@ -135,7 +132,7 @@ mod tests { let system = IntoSystem::into_system(|_world: &mut World, name: SystemName| name.name().to_owned()) .with_name("testing"); - let name = world.run_system_once(system).unwrap(); + let name = world.run_system_once(system).unwrap().as_string(); assert_eq!(name, "testing"); } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index fc795abf59..d86d71b9d6 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -25,6 +25,7 @@ use alloc::{ pub use bevy_ecs_macros::SystemParam; use bevy_platform::cell::SyncCell; use bevy_ptr::UnsafeCellDeref; +use bevy_utils::prelude::DebugName; use core::{ any::Any, fmt::{Debug, Display}, @@ -32,7 +33,6 @@ use core::{ ops::{Deref, DerefMut}, panic::Location, }; -use disqualified::ShortName; use thiserror::Error; use super::Populated; @@ -57,7 +57,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # use bevy_ecs::prelude::*; /// # #[derive(Resource)] /// # struct SomeResource; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct SomeEvent; /// # #[derive(Resource)] /// # struct SomeOtherResource; @@ -151,6 +151,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// let mut world = World::new(); /// let err = world.run_system_cached(|param: MyParam| {}).unwrap_err(); /// let expected = "Parameter `MyParam::foo` failed validation: Custom Message"; +/// # #[cfg(feature="Trace")] // Without debug_utils/debug enabled MyParam::foo is stripped and breaks the assert /// assert!(err.to_string().contains(expected)); /// ``` /// @@ -206,7 +207,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # Safety /// /// The implementor must ensure the following is true. -/// - [`SystemParam::init_state`] correctly registers all [`World`] accesses used +/// - [`SystemParam::init_access`] correctly registers all [`World`] accesses used /// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). /// - None of the world accesses may conflict with any prior accesses registered /// on `system_meta`. @@ -220,9 +221,16 @@ pub unsafe trait SystemParam: Sized { /// You could think of [`SystemParam::Item<'w, 's>`] as being an *operation* that changes the lifetimes bound to `Self`. type Item<'world, 'state>: SystemParam; + /// Creates a new instance of this param's [`State`](SystemParam::State). + fn init_state(world: &mut World) -> Self::State; + /// Registers any [`World`] access used by this [`SystemParam`] - /// and creates a new instance of this param's [`State`](SystemParam::State). - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State; + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ); /// Applies any deferred mutations stored in this [`SystemParam`]'s state. /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). @@ -274,7 +282,7 @@ pub unsafe trait SystemParam: Sized { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have read-only access to world data - /// registered in [`init_state`](SystemParam::init_state). + /// registered in [`init_access`](SystemParam::init_access). /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). #[expect( unused_variables, @@ -293,7 +301,7 @@ pub unsafe trait SystemParam: Sized { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have access to any world data registered - /// in [`init_state`](SystemParam::init_state). + /// in [`init_access`](SystemParam::init_access). /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, @@ -324,10 +332,25 @@ unsafe impl SystemParam for Qu type State = QueryState; type Item<'w, 's> = Query<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let state = QueryState::new(world); - init_query_param(world, system_meta, &state); - state + fn init_state(world: &mut World) -> Self::State { + QueryState::new(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + assert_component_access_compatibility( + &system_meta.name, + DebugName::type_name::(), + DebugName::type_name::(), + component_access_set, + &state.component_access, + world, + ); + component_access_set.add(state.component_access.clone()); } #[inline] @@ -345,28 +368,10 @@ unsafe impl SystemParam for Qu } } -pub(crate) fn init_query_param( - world: &mut World, - system_meta: &mut SystemMeta, - state: &QueryState, -) { - assert_component_access_compatibility( - &system_meta.name, - core::any::type_name::(), - core::any::type_name::(), - &system_meta.component_access_set, - &state.component_access, - world, - ); - system_meta - .component_access_set - .add(state.component_access.clone()); -} - fn assert_component_access_compatibility( - system_name: &str, - query_type: &'static str, - filter_type: &'static str, + system_name: &DebugName, + query_type: DebugName, + filter_type: DebugName, system_access: &FilteredAccessSet, current: &FilteredAccess, world: &World, @@ -380,17 +385,28 @@ fn assert_component_access_compatibility( if !accesses.is_empty() { accesses.push(' '); } - panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001", ShortName(query_type), ShortName(filter_type)); + panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001", query_type.shortname(), filter_type.shortname()); } // SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. -unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam for Single<'a, D, F> { +unsafe impl<'a, 'b, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam + for Single<'a, 'b, D, F> +{ type State = QueryState; - type Item<'w, 's> = Single<'w, D, F>; + type Item<'w, 's> = Single<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Query::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + Query::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -438,8 +454,8 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo } // SAFETY: QueryState is constrained to read-only fetches, so it only reads World. -unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam - for Single<'a, D, F> +unsafe impl<'a, 'b, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam + for Single<'a, 'b, D, F> { } @@ -451,8 +467,17 @@ unsafe impl SystemParam type State = QueryState; type Item<'w, 's> = Populated<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Query::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + Query::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -576,7 +601,7 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # impl MyEvent { /// # pub fn new() -> Self { Self } @@ -616,7 +641,7 @@ pub struct ParamSet<'w, 's, T: SystemParam> { } macro_rules! impl_param_set { - ($(($index: tt, $param: ident, $system_meta: ident, $fn_name: ident)),*) => { + ($(($index: tt, $param: ident, $fn_name: ident)),*) => { // SAFETY: All parameters are constrained to ReadOnlySystemParam, so World is only read unsafe impl<'w, 's, $($param,)*> ReadOnlySystemParam for ParamSet<'w, 's, ($($param,)*)> where $($param: ReadOnlySystemParam,)* @@ -637,25 +662,32 @@ macro_rules! impl_param_set { non_snake_case, reason = "Certain variable names are provided by the caller, not by us." )] - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + ($($param::init_state(world),)*) + } + + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro meant for tuples; as such, `non_snake_case` won't always lint." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] + fn init_access(state: &Self::State, system_meta: &mut SystemMeta, component_access_set: &mut FilteredAccessSet, world: &mut World) { + let ($($param,)*) = state; $( - // Pretend to add each param to the system alone, see if it conflicts - let mut $system_meta = system_meta.clone(); - $system_meta.component_access_set.clear(); - $param::init_state(world, &mut $system_meta); - // The variable is being defined with non_snake_case here - let $param = $param::init_state(world, &mut system_meta.clone()); + // Call `init_access` on a clone of the original access set to check for conflicts + let component_access_set_clone = &mut component_access_set.clone(); + $param::init_access($param, system_meta, component_access_set_clone, world); )* - // Make the ParamSet non-send if any of its parameters are non-send. - if false $(|| !$system_meta.is_send())* { - system_meta.set_non_send(); - } $( - system_meta - .component_access_set - .extend($system_meta.component_access_set); + // Pretend to add the param to the system alone to gather the new access, + // then merge its access into the system. + let mut access_set = FilteredAccessSet::new(); + $param::init_access($param, system_meta, &mut access_set, world); + component_access_set.extend(access_set); )* - ($($param,)*) } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -711,7 +743,7 @@ macro_rules! impl_param_set { } } -all_tuples_enumerated!(impl_param_set, 1, 8, P, m, p); +all_tuples_enumerated!(impl_param_set, 1, 8, P, p); // SAFETY: Res only reads a single World resource unsafe impl<'a, T: Resource> ReadOnlySystemParam for Res<'a, T> {} @@ -722,21 +754,25 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { type State = ComponentId; type Item<'w, 's> = Res<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let component_id = world.components_registrator().register_resource::(); + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_resource::() + } - let combined_access = system_meta.component_access_set.combined_access(); + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] @@ -773,8 +809,8 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { panic!( "Resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); Res { value: ptr.deref(), @@ -795,24 +831,27 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { type State = ComponentId; type Item<'w, 's> = ResMut<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let component_id = world.components_registrator().register_resource::(); + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_resource::() + } - let combined_access = system_meta.component_access_set.combined_access(); + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); if combined_access.has_resource_write(component_id) { panic!( "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } else if combined_access.has_resource_read(component_id) { panic!( "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] @@ -848,8 +887,8 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { panic!( "Resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); ResMut { value: value.value.deref_mut::(), @@ -872,18 +911,24 @@ unsafe impl SystemParam for &'_ World { type State = (); type Item<'w, 's> = &'w World; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { let mut filtered_access = FilteredAccess::default(); filtered_access.read_all(); - if !system_meta - .component_access_set + if !component_access_set .get_conflicts_single(&filtered_access) .is_empty() { panic!("&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules"); } - system_meta.component_access_set.add(filtered_access); + component_access_set.add(filtered_access); } #[inline] @@ -903,16 +948,20 @@ unsafe impl<'w> SystemParam for DeferredWorld<'w> { type State = (); type Item<'world, 'state> = DeferredWorld<'world>; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { assert!( - !system_meta - .component_access_set - .combined_access() - .has_any_read(), + !component_access_set.combined_access().has_any_read(), "DeferredWorld in system {} conflicts with a previous access.", system_meta.name, ); - system_meta.component_access_set.write_all(); + component_access_set.write_all(); } unsafe fn get_param<'world, 'state>( @@ -1042,10 +1091,18 @@ unsafe impl<'a, T: FromWorld + Send + 'static> SystemParam for Local<'a, T> { type State = SyncCell; type Item<'w, 's> = Local<'s, T>; - fn init_state(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { SyncCell::new(T::from_world(world)) } + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } + #[inline] unsafe fn get_param<'w, 's>( state: &'s mut Self::State, @@ -1222,11 +1279,19 @@ unsafe impl SystemParam for Deferred<'_, T> { type State = SyncCell; type Item<'w, 's> = Deferred<'s, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.set_has_deferred(); + fn init_state(world: &mut World) -> Self::State { SyncCell::new(T::from_world(world)) } + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + system_meta.set_has_deferred(); + } + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { state.get().apply(system_meta, world); } @@ -1255,7 +1320,14 @@ unsafe impl SystemParam for NonSendMarker { type Item<'w, 's> = Self; #[inline] - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); } @@ -1328,6 +1400,7 @@ impl<'w, T> Deref for NonSend<'w, T> { self.value } } + impl<'a, T> From> for NonSend<'a, T> { fn from(nsm: NonSendMut<'a, T>) -> Self { Self { @@ -1349,23 +1422,26 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { type State = ComponentId; type Item<'w, 's> = NonSend<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_non_send::() + } + + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); - let component_id = world.components_registrator().register_non_send::(); - - let combined_access = system_meta.component_access_set.combined_access(); + let combined_access = component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] @@ -1402,7 +1478,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { panic!( "Non-send resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() + DebugName::type_name::() ) }); @@ -1422,26 +1498,29 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { type State = ComponentId; type Item<'w, 's> = NonSendMut<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_non_send::() + } + + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); - let component_id = world.components_registrator().register_non_send::(); - - let combined_access = system_meta.component_access_set.combined_access(); + let combined_access = component_access_set.combined_access(); if combined_access.has_component_write(component_id) { panic!( "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } else if combined_access.has_component_read(component_id) { panic!( "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] @@ -1478,8 +1557,8 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { panic!( "Non-send resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); NonSendMut { value: ptr.assert_unique().deref_mut(), @@ -1497,7 +1576,15 @@ unsafe impl<'a> SystemParam for &'a Archetypes { type State = (); type Item<'w, 's> = &'w Archetypes; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1518,7 +1605,15 @@ unsafe impl<'a> SystemParam for &'a Components { type State = (); type Item<'w, 's> = &'w Components; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1539,7 +1634,15 @@ unsafe impl<'a> SystemParam for &'a Entities { type State = (); type Item<'w, 's> = &'w Entities; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1560,7 +1663,15 @@ unsafe impl<'a> SystemParam for &'a Bundles { type State = (); type Item<'w, 's> = &'w Bundles; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1610,7 +1721,15 @@ unsafe impl SystemParam for SystemChangeTick { type State = (); type Item<'w, 's> = SystemChangeTick; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1632,8 +1751,17 @@ unsafe impl SystemParam for Option { type Item<'world, 'state> = Option>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1666,8 +1794,17 @@ unsafe impl SystemParam for Result = Result, SystemParamValidationError>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1752,8 +1889,17 @@ unsafe impl SystemParam for When { type Item<'world, 'state> = When>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1790,18 +1936,28 @@ unsafe impl SystemParam for When { // SAFETY: Delegates to `T`, which ensures the safety requirements are met unsafe impl ReadOnlySystemParam for When {} -// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Registers access for each element of `state`. +// If any one conflicts, it will panic. unsafe impl SystemParam for Vec { type State = Vec; type Item<'world, 'state> = Vec>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Vec::new() } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + for state in state { + T::init_access(state, system_meta, component_access_set, world); + } + } + #[inline] unsafe fn validate_param( state: &mut Self::State, @@ -1824,7 +1980,7 @@ unsafe impl SystemParam for Vec { state .iter_mut() // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by each param. + // - We initialized the access for each parameter in `init_access`, so the caller ensures we have access to any world data needed by each param. // - The caller ensures this was the world used to initialize our state, and we used that world to initialize parameter states .map(|state| unsafe { T::get_param(state, system_meta, world, change_tick) }) .collect() @@ -1843,18 +1999,38 @@ unsafe impl SystemParam for Vec { } } -// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Registers access for each element of `state`. +// If any one conflicts with a previous parameter, +// the call passing a copy of the current access will panic. unsafe impl SystemParam for ParamSet<'_, '_, Vec> { type State = Vec; type Item<'world, 'state> = ParamSet<'world, 'state, Vec>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Vec::new() } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + for state in state { + // Call `init_access` on a clone of the original access set to check for conflicts + let component_access_set_clone = &mut component_access_set.clone(); + T::init_access(state, system_meta, component_access_set_clone, world); + } + for state in state { + // Pretend to add the param to the system alone to gather the new access, + // then merge its access into the system. + let mut access_set = FilteredAccessSet::new(); + T::init_access(state, system_meta, &mut access_set, world); + component_access_set.extend(access_set); + } + } + #[inline] unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, @@ -1888,7 +2064,7 @@ impl ParamSet<'_, '_, Vec> { /// No other parameters may be accessed while this one is active. pub fn get_mut(&mut self, index: usize) -> T::Item<'_, '_> { // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // - We initialized the access for each parameter, so the caller ensures we have access to any world data needed by any param. // We have mutable access to the ParamSet, so no other params in the set are active. // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states unsafe { @@ -1906,7 +2082,7 @@ impl ParamSet<'_, '_, Vec> { self.param_states.iter_mut().for_each(|state| { f( // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // - We initialized the access for each parameter, so the caller ensures we have access to any world data needed by any param. // We have mutable access to the ParamSet, so no other params in the set are active. // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states unsafe { T::get_param(state, &self.system_meta, self.world, self.change_tick) }, @@ -1940,8 +2116,13 @@ macro_rules! impl_system_param_tuple { type Item<'w, 's> = ($($param::Item::<'w, 's>,)*); #[inline] - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - (($($param::init_state(world, system_meta),)*)) + fn init_state(world: &mut World) -> Self::State { + (($($param::init_state(world),)*)) + } + + fn init_access(state: &Self::State, _system_meta: &mut SystemMeta, _component_access_set: &mut FilteredAccessSet, _world: &mut World) { + let ($($param,)*) = state; + $($param::init_access($param, _system_meta, _component_access_set, _world);)* } #[inline] @@ -2109,8 +2290,17 @@ unsafe impl SystemParam for StaticSystemParam<'_, '_, type State = P::State; type Item<'world, 'state> = StaticSystemParam<'world, 'state, P>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - P::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + P::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + P::init_access(state, system_meta, component_access_set, world); } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -2147,7 +2337,15 @@ unsafe impl SystemParam for PhantomData { type State = (); type Item<'world, 'state> = Self; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'world, 'state>( @@ -2359,6 +2557,14 @@ trait DynParamState: Sync + Send + Any { /// Queues any deferred mutations to be applied at the next [`ApplyDeferred`](crate::prelude::ApplyDeferred). fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld); + /// Registers any [`World`] access used by this [`SystemParam`] + fn init_access( + &self, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ); + /// Refer to [`SystemParam::validate_param`]. /// /// # Safety @@ -2382,6 +2588,15 @@ impl DynParamState for ParamState { T::queue(&mut self.0, system_meta, world); } + fn init_access( + &self, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(&self.0, system_meta, component_access_set, world); + } + unsafe fn validate_param( &mut self, system_meta: &SystemMeta, @@ -2391,16 +2606,27 @@ impl DynParamState for ParamState { } } -// SAFETY: `init_state` creates a state of (), which performs no access. The interesting safety checks are on the `SystemParamBuilder`. +// SAFETY: Delegates to the wrapped parameter, which ensures the safety requirements are met unsafe impl SystemParam for DynSystemParam<'_, '_> { type State = DynSystemParamState; type Item<'world, 'state> = DynSystemParam<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { DynSystemParamState::new::<()>(()) } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + state + .0 + .init_access(system_meta, component_access_set, world); + } + #[inline] unsafe fn validate_param( state: &mut Self::State, @@ -2419,8 +2645,8 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { ) -> Self::Item<'world, 'state> { // SAFETY: // - `state.0` is a boxed `ParamState`. - // - The state was obtained from `SystemParamBuilder::build()`, which registers all [`World`] accesses used - // by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). + // - `init_access` calls `DynParamState::init_access`, which calls `init_access` on the inner parameter, + // so the caller ensures the world has the necessary access. // - The caller ensures that the provided world is the same and has the required access. unsafe { DynSystemParam::new(state.0.as_mut(), world, system_meta.clone(), change_tick) } } @@ -2434,26 +2660,48 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { } } -// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResources` with no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Resource ComponentId access is applied to the access. If this FilteredResources +// conflicts with any prior access, a panic will occur. unsafe impl SystemParam for FilteredResources<'_, '_> { type State = Access; type Item<'world, 'state> = FilteredResources<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Access::new() } + fn init_access( + access: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &system_meta.name; + panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); + } + + if access.has_read_all_resources() { + component_access_set.add_unfiltered_read_all_resources(); + } else { + for component_id in access.resource_reads_and_writes() { + component_access_set.add_unfiltered_resource_read(component_id); + } + } + } + unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, - // and the builder registers `access` in `build`. + // SAFETY: The caller ensures that `world` has access to anything registered in `init_access`, + // and we registered all resource access in `state``. unsafe { FilteredResources::new(world, state, system_meta.last_run, change_tick) } } } @@ -2461,26 +2709,56 @@ unsafe impl SystemParam for FilteredResources<'_, '_> { // SAFETY: FilteredResources only reads resources. unsafe impl ReadOnlySystemParam for FilteredResources<'_, '_> {} -// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResourcesMut` with no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Resource ComponentId access is applied to the access. If this FilteredResourcesMut +// conflicts with any prior access, a panic will occur. unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { type State = Access; type Item<'world, 'state> = FilteredResourcesMut<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Access::new() } + fn init_access( + access: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &system_meta.name; + panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); + } + + if access.has_read_all_resources() { + component_access_set.add_unfiltered_read_all_resources(); + } else { + for component_id in access.resource_reads() { + component_access_set.add_unfiltered_resource_read(component_id); + } + } + + if access.has_write_all_resources() { + component_access_set.add_unfiltered_write_all_resources(); + } else { + for component_id in access.resource_writes() { + component_access_set.add_unfiltered_resource_write(component_id); + } + } + } + unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, - // and the builder registers `access` in `build`. + // SAFETY: The caller ensures that `world` has access to anything registered in `init_access`, + // and we registered all resource access in `state``. unsafe { FilteredResourcesMut::new(world, state, system_meta.last_run, change_tick) } } } @@ -2511,7 +2789,7 @@ pub struct SystemParamValidationError { /// A string identifying the invalid parameter. /// This is usually the type name of the parameter. - pub param: Cow<'static, str>, + pub param: DebugName, /// A string identifying the field within a parameter using `#[derive(SystemParam)]`. /// This will be an empty string for other parameters. @@ -2543,7 +2821,7 @@ impl SystemParamValidationError { Self { skipped, message: message.into(), - param: Cow::Borrowed(core::any::type_name::()), + param: DebugName::type_name::(), field: field.into(), } } @@ -2554,7 +2832,7 @@ impl Display for SystemParamValidationError { write!( fmt, "Parameter `{}{}` failed validation: {}", - ShortName(&self.param), + self.param.shortname(), self.field, self.message )?; @@ -2568,7 +2846,7 @@ impl Display for SystemParamValidationError { #[cfg(test)] mod tests { use super::*; - use crate::system::assert_is_system; + use crate::{event::Event, system::assert_is_system}; use core::cell::RefCell; // Compile test for https://github.com/bevyengine/bevy/pull/2838. @@ -2800,7 +3078,7 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_resource_error::res_system`: Parameter `Res` failed validation: Resource does not exist"] + #[should_panic] fn missing_resource_error() { #[derive(Resource)] pub struct MissingResource; @@ -2814,11 +3092,11 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: Event not initialized"] + #[should_panic] fn missing_event_error() { - use crate::prelude::{Event, EventReader}; + use crate::prelude::{BufferedEvent, EventReader}; - #[derive(Event)] + #[derive(Event, BufferedEvent)] pub struct MissingEvent; let mut schedule = crate::schedule::Schedule::default(); diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 272cc85d0d..e9c9cdba13 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -651,6 +651,19 @@ mod tests { assert_eq!(output, NonCopy(3)); } + #[test] + fn fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn exclusive_system() { let mut world = World::new(); @@ -751,19 +764,54 @@ mod tests { assert!(matches!(output, Ok(x) if x == four())); } + #[test] + fn cached_fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system_cached(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached(sys); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached_with(sys, ()); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn cached_system_commands() { fn sys(mut counter: ResMut) { - counter.0 = 1; + counter.0 += 1; } let mut world = World::new(); world.insert_resource(Counter(0)); - world.commands().run_system_cached(sys); world.flush_commands(); - assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); + } + + #[test] + fn cached_fallible_system_commands() { + fn sys(mut counter: ResMut) -> Result { + counter.0 += 1; + Ok(()) + } + + let mut world = World::new(); + world.insert_resource(Counter(0)); + world.commands().run_system_cached(sys); + world.flush_commands(); + assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); } #[test] @@ -865,7 +913,6 @@ mod tests { #[test] fn run_system_invalid_params() { use crate::system::RegisteredSystemError; - use alloc::{format, string::ToString}; struct T; impl Resource for T {} @@ -880,8 +927,6 @@ mod tests { result, Err(RegisteredSystemError::InvalidParams { .. }) )); - let expected = format!("System {id:?} did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."); - assert_eq!(expected, result.unwrap_err().to_string()); } #[test] diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 306ae7c92d..577720fd5d 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -1,6 +1,10 @@ //! A trait for components that let you traverse the ECS. -use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship}; +use crate::{ + entity::Entity, + query::{ReadOnlyQueryData, ReleaseStateQueryData}, + relationship::Relationship, +}; /// A component that can point to another entity, and which can be used to define a path through the ECS. /// @@ -14,19 +18,19 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// avoiding infinite loops in their code. /// /// Traversals may be parameterized with additional data. For example, in observer event propagation, the -/// parameter `D` is the event type given in `Trigger`. This allows traversal to differ depending on event +/// parameter `D` is the event type given in `On`. This allows traversal to differ depending on event /// data. /// -/// [specify the direction]: crate::event::Event::Traversal -/// [event propagation]: crate::observer::Trigger::propagate +/// [specify the direction]: crate::event::EntityEvent::Traversal +/// [event propagation]: crate::observer::On::propagate /// [observers]: crate::observer::Observer -pub trait Traversal: ReadOnlyQueryData { +pub trait Traversal: ReadOnlyQueryData + ReleaseStateQueryData { /// Returns the next entity to visit. - fn traverse(item: Self::Item<'_>, data: &D) -> Option; + fn traverse(item: Self::Item<'_, '_>, data: &D) -> Option; } impl Traversal for () { - fn traverse(_: Self::Item<'_>, _data: &D) -> Option { + fn traverse(_: Self::Item<'_, '_>, _data: &D) -> Option { None } } @@ -37,9 +41,9 @@ impl Traversal for () { /// /// Traversing in a loop could result in infinite loops for relationship graphs with loops. /// -/// [event propagation]: crate::observer::Trigger::propagate +/// [event propagation]: crate::observer::On::propagate impl Traversal for &R { - fn traverse(item: Self::Item<'_>, _data: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _data: &D) -> Option { Some(item.get()) } } diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index e8f820c066..243de1955c 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -423,12 +423,12 @@ mod test { let mut world = World::new(); queue.apply(&mut world); - assert_eq!(world.entities().len(), 2); + assert_eq!(world.entity_count(), 2); // The previous call to `apply` cleared the queue. // This call should do nothing. queue.apply(&mut world); - assert_eq!(world.entities().len(), 2); + assert_eq!(world.entity_count(), 2); } #[expect( @@ -462,7 +462,7 @@ mod test { queue.push(SpawnCommand); queue.push(SpawnCommand); queue.apply(&mut world); - assert_eq!(world.entities().len(), 3); + assert_eq!(world.entity_count(), 3); } #[test] diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index da51b8e09a..1699eadcff 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -1,12 +1,14 @@ use core::ops::Deref; +use bevy_utils::prelude::DebugName; + use crate::{ archetype::Archetype, change_detection::{MaybeLocation, MutUntyped}, component::{ComponentId, Mutable}, entity::Entity, - event::{Event, EventId, Events, SendBatchIds}, - lifecycle::{HookContext, ON_INSERT, ON_REPLACE}, + event::{BufferedEvent, EntityEvent, Event, EventId, Events, SendBatchIds}, + lifecycle::{HookContext, INSERT, REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, @@ -85,7 +87,7 @@ impl<'w> DeferredWorld<'w> { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -94,9 +96,11 @@ impl<'w> DeferredWorld<'w> { /// If you do not need to ensure the above hooks are triggered, and your component /// is mutable, prefer using [`get_mut`](DeferredWorld::get_mut). #[inline] - pub(crate) fn modify_component( + #[track_caller] + pub(crate) fn modify_component_with_relationship_hook_mode( &mut self, entity: Entity, + relationship_hook_mode: RelationshipHookMode, f: impl FnOnce(&mut T) -> R, ) -> Result, EntityMutableFetchError> { // If the component is not registered, then it doesn't exist on this entity, so no action required. @@ -104,18 +108,23 @@ impl<'w> DeferredWorld<'w> { return Ok(None); }; - self.modify_component_by_id(entity, component_id, move |component| { - // SAFETY: component matches the component_id collected in the above line - let mut component = unsafe { component.with_type::() }; + self.modify_component_by_id_with_relationship_hook_mode( + entity, + component_id, + relationship_hook_mode, + move |component| { + // SAFETY: component matches the component_id collected in the above line + let mut component = unsafe { component.with_type::() }; - f(&mut component) - }) + f(&mut component) + }, + ) } /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -124,13 +133,15 @@ impl<'w> DeferredWorld<'w> { /// If you do not need to ensure the above hooks are triggered, and your component /// is mutable, prefer using [`get_mut_by_id`](DeferredWorld::get_mut_by_id). /// - /// You should prefer the typed [`modify_component`](DeferredWorld::modify_component) + /// You should prefer the typed [`modify_component_with_relationship_hook_mode`](DeferredWorld::modify_component_with_relationship_hook_mode) /// whenever possible. #[inline] - pub(crate) fn modify_component_by_id( + #[track_caller] + pub(crate) fn modify_component_by_id_with_relationship_hook_mode( &mut self, entity: Entity, component_id: ComponentId, + relationship_hook_mode: RelationshipHookMode, f: impl for<'a> FnOnce(MutUntyped<'a>) -> R, ) -> Result, EntityMutableFetchError> { let entity_cell = self.get_entity_mut(entity)?; @@ -145,7 +156,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_replace( @@ -153,11 +164,11 @@ impl<'w> DeferredWorld<'w> { entity, [component_id].into_iter(), MaybeLocation::caller(), - RelationshipHookMode::Run, + relationship_hook_mode, ); if archetype.has_replace_observer() { self.trigger_observers( - ON_REPLACE, + REPLACE, Some(entity), [component_id].into_iter(), MaybeLocation::caller(), @@ -185,7 +196,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_insert( @@ -193,11 +204,11 @@ impl<'w> DeferredWorld<'w> { entity, [component_id].into_iter(), MaybeLocation::caller(), - RelationshipHookMode::Run, + relationship_hook_mode, ); if archetype.has_insert_observer() { self.trigger_observers( - ON_INSERT, + INSERT, Some(entity), [component_id].into_iter(), MaybeLocation::caller(), @@ -451,7 +462,7 @@ impl<'w> DeferredWorld<'w> { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -480,7 +491,7 @@ impl<'w> DeferredWorld<'w> { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -496,34 +507,34 @@ impl<'w> DeferredWorld<'w> { unsafe { self.world.get_non_send_resource_mut() } } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { let Some(mut events_resource) = self.get_resource_mut::>() else { log::error!( "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", - core::any::type_name::() + DebugName::type_name::() ); return None; }; @@ -747,6 +758,7 @@ impl<'w> DeferredWorld<'w> { self.reborrow(), event, target, + target, components, &mut (), &mut false, @@ -762,7 +774,8 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers_with_data( &mut self, event: ComponentId, - target: Option, + current_target: Option, + original_target: Option, components: impl Iterator + Clone, data: &mut E, mut propagate: bool, @@ -773,32 +786,36 @@ impl<'w> DeferredWorld<'w> { Observers::invoke::<_>( self.reborrow(), event, - target, + current_target, + original_target, components.clone(), data, &mut propagate, caller, ); - let Some(mut target) = target else { return }; + let Some(mut current_target) = current_target else { + return; + }; loop { if !propagate { return; } if let Some(traverse_to) = self - .get_entity(target) + .get_entity(current_target) .ok() .and_then(|entity| entity.get_components::()) .and_then(|item| T::traverse(item, data)) { - target = traverse_to; + current_target = traverse_to; } else { break; } Observers::invoke::<_>( self.reborrow(), event, - Some(target), + Some(current_target), + original_target, components.clone(), data, &mut propagate, @@ -807,15 +824,23 @@ impl<'w> DeferredWorld<'w> { } } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a global [`Event`] without any targets. + /// + /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger(&mut self, trigger: impl Event) { self.commands().trigger(trigger); } - /// Sends a [`Trigger`](crate::observer::Trigger) with the given `targets`. + /// Sends an [`EntityEvent`] with the given `targets` + /// + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger_targets( &mut self, - trigger: impl Event, + trigger: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.commands().trigger_targets(trigger, targets); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index af95bbf48d..9b7f8eb551 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -11,12 +11,12 @@ use crate::{ }, entity::{ ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, - EntityIdLocation, EntityLocation, + EntityIdLocation, EntityLocation, OptIn, OptOut, }, - event::Event, - lifecycle::{ON_DESPAWN, ON_REMOVE, ON_REPLACE}, + event::EntityEvent, + lifecycle::{DESPAWN, REMOVE, REPLACE}, observer::Observer, - query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, + query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, relationship::RelationshipHookMode, resource::Resource, system::IntoObserverSystem, @@ -279,14 +279,16 @@ impl<'w> EntityRef<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'w> { + pub fn components(&self) -> Q::Item<'w, 'static> { self.get_components::() .expect("Query does not match the current entity") } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { // SAFETY: We have read-only access to all components of this entity. unsafe { self.cell.get_components::() } } @@ -546,13 +548,15 @@ impl<'w> EntityMut<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } @@ -1310,7 +1314,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity does not have the components required by the query `Q` or if the entity /// has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } @@ -1321,7 +1325,9 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } @@ -1377,7 +1383,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1430,7 +1436,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -2368,7 +2374,7 @@ impl<'w> EntityWorldMut<'w> { unsafe { if archetype.has_despawn_observer() { deferred_world.trigger_observers( - ON_DESPAWN, + DESPAWN, Some(self.entity), archetype.components(), caller, @@ -2382,7 +2388,7 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, + REPLACE, Some(self.entity), archetype.components(), caller, @@ -2397,7 +2403,7 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, + REMOVE, Some(self.entity), archetype.components(), caller, @@ -2626,7 +2632,7 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.assert_not_despawned(); self.world.trigger_targets(event, self.entity); self.world.flush(); @@ -2643,14 +2649,14 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the given system is an exclusive system. #[track_caller] - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { self.observe_with_caller(observer, MaybeLocation::caller()) } - pub(crate) fn observe_with_caller( + pub(crate) fn observe_with_caller( &mut self, observer: impl IntoObserverSystem, caller: MaybeLocation, @@ -2666,10 +2672,12 @@ impl<'w> EntityWorldMut<'w> { /// Clones parts of an entity (components, observers, etc.) onto another entity, /// configured through [`EntityClonerBuilder`]. /// - /// By default, the other entity will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The other entity will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. + /// + /// # Example /// - /// Configure through [`EntityClonerBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; /// # #[derive(Component, Clone, PartialEq, Debug)] @@ -2679,27 +2687,76 @@ impl<'w> EntityWorldMut<'w> { /// # let mut world = World::new(); /// # let entity = world.spawn((ComponentA, ComponentB)).id(); /// # let target = world.spawn_empty().id(); - /// world.entity_mut(entity).clone_with(target, |builder| { - /// builder.deny::(); + /// // Clone all components except ComponentA onto the target. + /// world.entity_mut(entity).clone_with_opt_out(target, |builder| { + /// builder.deny::(); /// }); - /// # assert_eq!(world.get::(target), Some(&ComponentA)); - /// # assert_eq!(world.get::(target), None); + /// # assert_eq!(world.get::(target), None); + /// # assert_eq!(world.get::(target), Some(&ComponentB)); /// ``` /// - /// See [`EntityClonerBuilder`] for more options. + /// See [`EntityClonerBuilder`] for more options. /// /// # Panics /// /// - If this entity has been despawned while this `EntityWorldMut` is still alive. /// - If the target entity does not exist. - pub fn clone_with( + pub fn clone_with_opt_out( &mut self, target: Entity, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> &mut Self { self.assert_not_despawned(); - let mut builder = EntityCloner::build(self.world); + let mut builder = EntityCloner::build_opt_out(self.world); + config(&mut builder); + builder.clone_entity(self.entity, target); + + self.world.flush(); + self.update_location(); + self + } + + /// Clones parts of an entity (components, observers, etc.) onto another entity, + /// configured through [`EntityClonerBuilder`]. + /// + /// The other entity will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentA; + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentB; + /// # let mut world = World::new(); + /// # let entity = world.spawn((ComponentA, ComponentB)).id(); + /// # let target = world.spawn_empty().id(); + /// // Clone only ComponentA onto the target. + /// world.entity_mut(entity).clone_with_opt_in(target, |builder| { + /// builder.allow::(); + /// }); + /// # assert_eq!(world.get::(target), Some(&ComponentA)); + /// # assert_eq!(world.get::(target), None); + /// ``` + /// + /// See [`EntityClonerBuilder`] for more options. + /// + /// # Panics + /// + /// - If this entity has been despawned while this `EntityWorldMut` is still alive. + /// - If the target entity does not exist. + pub fn clone_with_opt_in( + &mut self, + target: Entity, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> &mut Self { + self.assert_not_despawned(); + + let mut builder = EntityCloner::build_opt_in(self.world); config(&mut builder); builder.clone_entity(self.entity, target); @@ -2714,52 +2771,104 @@ impl<'w> EntityWorldMut<'w> { /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). /// /// To configure cloning behavior (such as only cloning certain components), - /// use [`EntityWorldMut::clone_and_spawn_with`]. + /// use [`EntityWorldMut::clone_and_spawn_with_opt_out`]/ + /// [`opt_in`](`EntityWorldMut::clone_and_spawn_with_opt_in`). /// /// # Panics /// /// If this entity has been despawned while this `EntityWorldMut` is still alive. pub fn clone_and_spawn(&mut self) -> Entity { - self.clone_and_spawn_with(|_| {}) + self.clone_and_spawn_with_opt_out(|_| {}) } /// Spawns a clone of this entity and allows configuring cloning behavior /// using [`EntityClonerBuilder`], returning the [`Entity`] of the clone. /// - /// By default, the clone will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The clone will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. + /// + /// # Example /// - /// Configure through [`EntityClonerBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let entity = world.spawn((ComponentA, ComponentB)).id(); /// # #[derive(Component, Clone, PartialEq, Debug)] /// # struct ComponentA; /// # #[derive(Component, Clone, PartialEq, Debug)] /// # struct ComponentB; - /// # let mut world = World::new(); - /// # let entity = world.spawn((ComponentA, ComponentB)).id(); - /// let entity_clone = world.entity_mut(entity).clone_and_spawn_with(|builder| { - /// builder.deny::(); + /// // Create a clone of an entity but without ComponentA. + /// let entity_clone = world.entity_mut(entity).clone_and_spawn_with_opt_out(|builder| { + /// builder.deny::(); /// }); - /// # assert_eq!(world.get::(entity_clone), Some(&ComponentA)); - /// # assert_eq!(world.get::(entity_clone), None); + /// # assert_eq!(world.get::(entity_clone), None); + /// # assert_eq!(world.get::(entity_clone), Some(&ComponentB)); /// ``` /// - /// See [`EntityClonerBuilder`] for more options. + /// See [`EntityClonerBuilder`] for more options. /// /// # Panics /// /// If this entity has been despawned while this `EntityWorldMut` is still alive. - pub fn clone_and_spawn_with( + pub fn clone_and_spawn_with_opt_out( &mut self, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> Entity { self.assert_not_despawned(); let entity_clone = self.world.entities.reserve_entity(); self.world.flush(); - let mut builder = EntityCloner::build(self.world); + let mut builder = EntityCloner::build_opt_out(self.world); + config(&mut builder); + builder.clone_entity(self.entity, entity_clone); + + self.world.flush(); + self.update_location(); + entity_clone + } + + /// Spawns a clone of this entity and allows configuring cloning behavior + /// using [`EntityClonerBuilder`], returning the [`Entity`] of the clone. + /// + /// The clone will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let entity = world.spawn((ComponentA, ComponentB)).id(); + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentA; + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentB; + /// // Create a clone of an entity but only with ComponentA. + /// let entity_clone = world.entity_mut(entity).clone_and_spawn_with_opt_in(|builder| { + /// builder.allow::(); + /// }); + /// # assert_eq!(world.get::(entity_clone), Some(&ComponentA)); + /// # assert_eq!(world.get::(entity_clone), None); + /// ``` + /// + /// See [`EntityClonerBuilder`] for more options. + /// + /// # Panics + /// + /// If this entity has been despawned while this `EntityWorldMut` is still alive. + pub fn clone_and_spawn_with_opt_in( + &mut self, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> Entity { + self.assert_not_despawned(); + + let entity_clone = self.world.entities.reserve_entity(); + self.world.flush(); + + let mut builder = EntityCloner::build_opt_in(self.world); config(&mut builder); builder.clone_entity(self.entity, entity_clone); @@ -2780,8 +2889,7 @@ impl<'w> EntityWorldMut<'w> { pub fn clone_components(&mut self, target: Entity) -> &mut Self { self.assert_not_despawned(); - EntityCloner::build(self.world) - .deny_all() + EntityCloner::build_opt_in(self.world) .allow::() .clone_entity(self.entity, target); @@ -2803,8 +2911,7 @@ impl<'w> EntityWorldMut<'w> { pub fn move_components(&mut self, target: Entity) -> &mut Self { self.assert_not_despawned(); - EntityCloner::build(self.world) - .deny_all() + EntityCloner::build_opt_in(self.world) .allow::() .move_components(true) .clone_entity(self.entity, target); @@ -5739,7 +5846,7 @@ mod tests { assert_eq!((&mut X(8), &mut Y(9)), (x_component, y_component)); } - #[derive(Event)] + #[derive(Event, EntityEvent)] struct TestEvent; #[test] @@ -5747,10 +5854,8 @@ mod tests { let mut world = World::new(); let entity = world .spawn_empty() - .observe(|trigger: Trigger, mut commands: Commands| { - commands - .entity(trigger.target().unwrap()) - .insert(TestComponent(0)); + .observe(|trigger: On, mut commands: Commands| { + commands.entity(trigger.target()).insert(TestComponent(0)); }) .id(); @@ -5759,7 +5864,7 @@ mod tests { let mut a = world.entity_mut(entity); a.trigger(TestEvent); // this adds command to change entity archetype - a.observe(|_: Trigger| {}); // this flushes commands implicitly by spawning + a.observe(|_: On| {}); // this flushes commands implicitly by spawning let location = a.location(); assert_eq!(world.entities().get(entity), Some(location)); } @@ -5768,11 +5873,9 @@ mod tests { #[should_panic] fn location_on_despawned_entity_panics() { let mut world = World::new(); - world.add_observer( - |trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target().unwrap()).despawn(); - }, - ); + world.add_observer(|trigger: On, mut commands: Commands| { + commands.entity(trigger.target()).despawn(); + }); let entity = world.spawn_empty().id(); let mut a = world.entity_mut(entity); a.insert(TestComponent(0)); @@ -5790,14 +5893,12 @@ mod tests { fn archetype_modifications_trigger_flush() { let mut world = World::new(); world.insert_resource(TestFlush(0)); - world.add_observer(|_: Trigger, mut commands: Commands| { + world.add_observer(|_: On, mut commands: Commands| { + commands.queue(count_flush); + }); + world.add_observer(|_: On, mut commands: Commands| { commands.queue(count_flush); }); - world.add_observer( - |_: Trigger, mut commands: Commands| { - commands.queue(count_flush); - }, - ); world.commands().queue(count_flush); let entity = world.spawn_empty().id(); assert_eq!(world.resource::().0, 1); @@ -5862,19 +5963,19 @@ mod tests { .push("OrdA hook on_remove"); } - fn ord_a_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_add"); } - fn ord_a_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_insert"); } - fn ord_a_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_replace"); } - fn ord_a_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_remove"); } @@ -5913,19 +6014,19 @@ mod tests { .push("OrdB hook on_remove"); } - fn ord_b_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_add"); } - fn ord_b_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_insert"); } - fn ord_b_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_replace"); } - fn ord_b_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_remove"); } @@ -5998,12 +6099,12 @@ mod tests { #[test] fn entity_world_mut_clone_with_move_and_require() { #[derive(Component, Clone, PartialEq, Debug)] - #[require(B)] + #[require(B(3))] struct A; #[derive(Component, Clone, PartialEq, Debug, Default)] #[require(C(3))] - struct B; + struct B(u32); #[derive(Component, Clone, PartialEq, Debug, Default)] #[require(D)] @@ -6013,22 +6114,25 @@ mod tests { struct D; let mut world = World::new(); - let entity_a = world.spawn(A).id(); + let entity_a = world.spawn((A, B(5))).id(); let entity_b = world.spawn_empty().id(); - world.entity_mut(entity_a).clone_with(entity_b, |builder| { - builder - .move_components(true) - .without_required_components(|builder| { - builder.deny::(); - }); - }); + world + .entity_mut(entity_a) + .clone_with_opt_in(entity_b, |builder| { + builder + .move_components(true) + .allow::() + .without_required_components(|builder| { + builder.allow::(); + }); + }); - assert_eq!(world.entity(entity_a).get::(), Some(&A)); - assert_eq!(world.entity(entity_b).get::(), None); + assert_eq!(world.entity(entity_a).get::(), None); + assert_eq!(world.entity(entity_b).get::(), Some(&A)); - assert_eq!(world.entity(entity_a).get::(), None); - assert_eq!(world.entity(entity_b).get::(), Some(&B)); + assert_eq!(world.entity(entity_a).get::(), Some(&B(5))); + assert_eq!(world.entity(entity_b).get::(), Some(&B(3))); assert_eq!(world.entity(entity_a).get::(), None); assert_eq!(world.entity(entity_b).get::(), Some(&C(3))); @@ -6201,10 +6305,10 @@ mod tests { world.insert_resource(Tracker { a: false, b: false }); let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.a = true; }); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.b = true; }); diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 3527967942..03574331f2 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -1,6 +1,7 @@ //! Contains error types returned by bevy's schedule. use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use crate::{ component::ComponentId, @@ -24,7 +25,7 @@ pub struct TryRunScheduleError(pub InternedScheduleLabel); #[error("Could not insert bundles of type {bundle_type} into the entities with the following IDs because they do not exist: {entities:?}")] pub struct TryInsertBatchError { /// The bundles' type name. - pub bundle_type: &'static str, + pub bundle_type: DebugName, /// The IDs of the provided entities that do not exist. pub entities: Vec, } diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index 6b1c803e75..51f9a0ee2c 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -1,5 +1,6 @@ use crate::{ - component::Tick, + component::{ComponentId, Tick}, + query::FilteredAccessSet, storage::SparseSetIndex, system::{ExclusiveSystemParam, ReadOnlySystemParam, SystemMeta, SystemParam}, world::{FromWorld, World}, @@ -53,7 +54,15 @@ unsafe impl SystemParam for WorldId { type Item<'world, 'state> = WorldId; - fn init_state(_: &mut World, _: &mut SystemMeta) -> Self::State {} + fn init_state(_: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'world, 'state>( diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 34d4cd9c4a..714c5e1eae 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -19,10 +19,12 @@ pub use crate::{ }; use crate::{ error::{DefaultErrorHandler, ErrorHandler}, - lifecycle::{ComponentHooks, ON_ADD, ON_DESPAWN, ON_INSERT, ON_REMOVE, ON_REPLACE}, - prelude::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace}, + event::BufferedEvent, + lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, + prelude::{Add, Despawn, Insert, Remove, Replace}, }; pub use bevy_ecs_macros::FromWorld; +use bevy_utils::prelude::DebugName; pub use deferred_world::DeferredWorld; pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_ref::{ @@ -150,20 +152,20 @@ impl World { #[inline] fn bootstrap(&mut self) { // The order that we register these events is vital to ensure that the constants are correct! - let on_add = OnAdd::register_component_id(self); - assert_eq!(ON_ADD, on_add); + let on_add = Add::register_component_id(self); + assert_eq!(ADD, on_add); - let on_insert = OnInsert::register_component_id(self); - assert_eq!(ON_INSERT, on_insert); + let on_insert = Insert::register_component_id(self); + assert_eq!(INSERT, on_insert); - let on_replace = OnReplace::register_component_id(self); - assert_eq!(ON_REPLACE, on_replace); + let on_replace = Replace::register_component_id(self); + assert_eq!(REPLACE, on_replace); - let on_remove = OnRemove::register_component_id(self); - assert_eq!(ON_REMOVE, on_remove); + let on_remove = Remove::register_component_id(self); + assert_eq!(REMOVE, on_remove); - let on_despawn = OnDespawn::register_component_id(self); - assert_eq!(ON_DESPAWN, on_despawn); + let on_despawn = Despawn::register_component_id(self); + assert_eq!(DESPAWN, on_despawn); // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); @@ -214,6 +216,14 @@ impl World { &mut self.entities } + /// Retrieves the number of [`Entities`] in the world. + /// + /// This is helpful as a diagnostic, but it can also be used effectively in tests. + #[inline] + pub fn entity_count(&self) -> u32 { + self.entities.len() + } + /// Retrieves this world's [`Archetypes`] collection. #[inline] pub fn archetypes(&self) -> &Archetypes { @@ -260,6 +270,12 @@ impl World { &self.removed_components } + /// Retrieves this world's [`Observers`] list + #[inline] + pub fn observers(&self) -> &Observers { + &self.observers + } + /// Creates a new [`Commands`] instance that writes to the world's command queue /// Use [`World::flush`] to apply all queued commands #[inline] @@ -1273,7 +1289,7 @@ impl World { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1303,6 +1319,7 @@ impl World { /// # assert_eq!(world.get::(entity), Some(&Foo(true))); /// ``` #[inline] + #[track_caller] pub fn modify_component( &mut self, entity: Entity, @@ -1310,7 +1327,11 @@ impl World { ) -> Result, EntityMutableFetchError> { let mut world = DeferredWorld::from(&mut *self); - let result = world.modify_component(entity, f)?; + let result = world.modify_component_with_relationship_hook_mode( + entity, + RelationshipHookMode::Run, + f, + )?; self.flush(); Ok(result) @@ -1319,7 +1340,7 @@ impl World { /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1331,6 +1352,7 @@ impl World { /// You should prefer the typed [`modify_component`](World::modify_component) /// whenever possible. #[inline] + #[track_caller] pub fn modify_component_by_id( &mut self, entity: Entity, @@ -1339,7 +1361,12 @@ impl World { ) -> Result, EntityMutableFetchError> { let mut world = DeferredWorld::from(&mut *self); - let result = world.modify_component_by_id(entity, component_id, f)?; + let result = world.modify_component_by_id_with_relationship_hook_mode( + entity, + component_id, + RelationshipHookMode::Run, + f, + )?; self.flush(); Ok(result) @@ -1941,7 +1968,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -1965,7 +1992,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -1989,7 +2016,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2153,7 +2180,7 @@ impl World { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2175,7 +2202,7 @@ impl World { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2342,11 +2369,11 @@ impl World { ) }; } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003", core::any::type_name::(), self.entities.entity_does_not_exist_error_details(entity)); + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003", DebugName::type_name::(), self.entities.entity_does_not_exist_error_details(entity)); } } } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {first_entity}, which {}. See: https://bevy.org/learn/errors/b0003", core::any::type_name::(), self.entities.entity_does_not_exist_error_details(first_entity)); + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {first_entity}, which {}. See: https://bevy.org/learn/errors/b0003", DebugName::type_name::(), self.entities.entity_does_not_exist_error_details(first_entity)); } } } @@ -2510,7 +2537,7 @@ impl World { Ok(()) } else { Err(TryInsertBatchError { - bundle_type: core::any::type_name::(), + bundle_type: DebugName::type_name::(), entities: invalid_entities, }) } @@ -2544,7 +2571,7 @@ impl World { #[track_caller] pub fn resource_scope(&mut self, f: impl FnOnce(&mut World, Mut) -> U) -> U { self.try_resource_scope(f) - .unwrap_or_else(|| panic!("resource does not exist: {}", core::any::type_name::())) + .unwrap_or_else(|| panic!("resource does not exist: {}", DebugName::type_name::())) } /// Temporarily removes the requested resource from this [`World`] if it exists, runs custom user code, @@ -2584,7 +2611,7 @@ impl World { assert!(!self.contains_resource::(), "Resource `{}` was inserted during a call to World::resource_scope.\n\ This is not allowed as the original resource is reinserted to the world after the closure is invoked.", - core::any::type_name::()); + DebugName::type_name::()); OwningPtr::make(value, |ptr| { // SAFETY: pointer is of type R @@ -2598,34 +2625,34 @@ impl World { Some(result) } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { let Some(mut events_resource) = self.get_resource_mut::>() else { log::error!( "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", - core::any::type_name::() + DebugName::type_name::() ); return None; }; @@ -2937,17 +2964,21 @@ impl World { } /// Iterates all component change ticks and clamps any older than [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). - /// This prevents overflow and thus prevents false positives. + /// This also triggers [`CheckChangeTicks`] observers and returns the same event here. /// - /// **Note:** Does nothing if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] + /// Calling this method prevents [`Tick`]s overflowing and thus prevents false positives when comparing them. + /// + /// **Note:** Does nothing and returns `None` if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] /// times since the previous pass. // TODO: benchmark and optimize - pub fn check_change_ticks(&mut self) { + pub fn check_change_ticks(&mut self) -> Option { let change_tick = self.change_tick(); if change_tick.relative_to(self.last_check_tick).get() < CHECK_TICK_THRESHOLD { - return; + return None; } + let check = CheckChangeTicks(change_tick); + let Storages { ref mut tables, ref mut sparse_sets, @@ -2957,19 +2988,22 @@ impl World { #[cfg(feature = "trace")] let _span = tracing::info_span!("check component ticks").entered(); - tables.check_change_ticks(change_tick); - sparse_sets.check_change_ticks(change_tick); - resources.check_change_ticks(change_tick); - non_send_resources.check_change_ticks(change_tick); - self.entities.check_change_ticks(change_tick); + tables.check_change_ticks(check); + sparse_sets.check_change_ticks(check); + resources.check_change_ticks(check); + non_send_resources.check_change_ticks(check); + self.entities.check_change_ticks(check); if let Some(mut schedules) = self.get_resource_mut::() { - schedules.check_change_ticks(change_tick); + schedules.check_change_ticks(check); } - self.trigger(CheckChangeTicks(change_tick)); + self.trigger(check); + self.flush(); self.last_check_tick = change_tick; + + Some(check) } /// Runs both [`clear_entities`](Self::clear_entities) and [`clear_resources`](Self::clear_resources), @@ -3619,6 +3653,7 @@ mod tests { }; use bevy_ecs_macros::Component; use bevy_platform::collections::{HashMap, HashSet}; + use bevy_utils::prelude::DebugName; use core::{ any::TypeId, panic, @@ -3802,12 +3837,12 @@ mod tests { let mut iter = world.iter_resources(); let (info, ptr) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource` assert_eq!(unsafe { ptr.deref::().0 }, 42); let (info, ptr) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); assert_eq!( // SAFETY: We know that the resource is of type `TestResource2` unsafe { &ptr.deref::().0 }, @@ -3830,14 +3865,14 @@ mod tests { let mut iter = world.iter_resources_mut(); let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource` unsafe { mut_untyped.as_mut().deref_mut::().0 = 43; }; let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource2` unsafe { mut_untyped.as_mut().deref_mut::().0 = "Hello, world?".to_string(); diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs index 5ecdf88156..aada63bf61 100644 --- a/crates/bevy_ecs/src/world/reflect.rs +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -4,8 +4,8 @@ use core::any::TypeId; use thiserror::Error; -use alloc::string::{String, ToString}; use bevy_reflect::{Reflect, ReflectFromPtr}; +use bevy_utils::prelude::DebugName; use crate::{prelude::*, world::ComponentId}; @@ -77,10 +77,7 @@ impl World { }; let Some(comp_ptr) = self.get_by_id(entity, component_id) else { - let component_name = self - .components() - .get_name(component_id) - .map(|name| name.to_string()); + let component_name = self.components().get_name(component_id); return Err(GetComponentReflectError::EntityDoesNotHaveComponent { entity, @@ -166,10 +163,7 @@ impl World { // HACK: Only required for the `None`-case/`else`-branch, but it borrows `self`, which will // already be mutably borrowed by `self.get_mut_by_id()`, and I didn't find a way around it. - let component_name = self - .components() - .get_name(component_id) - .map(|name| name.to_string()); + let component_name = self.components().get_name(component_id).clone(); let Some(comp_mut_untyped) = self.get_mut_by_id(entity, component_id) else { return Err(GetComponentReflectError::EntityDoesNotHaveComponent { @@ -223,7 +217,7 @@ pub enum GetComponentReflectError { component_id: ComponentId, /// The name corresponding to the [`Component`] with the given [`TypeId`], or `None` /// if not available. - component_name: Option, + component_name: Option, }, /// The [`World`] was missing the [`AppTypeRegistry`] resource. diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index ca898f9531..38d4333843 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -11,7 +11,7 @@ use crate::{ lifecycle::RemovedComponentEvents, observer::Observers, prelude::Component, - query::{DebugCheckedUnwrap, ReadOnlyQueryData}, + query::{DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -36,7 +36,7 @@ use thiserror::Error; /// /// This alone is not enough to implement bevy systems where multiple systems can access *disjoint* parts of the world concurrently. For this, bevy stores all values of /// resources and components (and [`ComponentTicks`]) in [`UnsafeCell`]s, and carefully validates disjoint access patterns using -/// APIs like [`System::component_access_set`](crate::system::System::component_access_set). +/// APIs like [`System::initialize`](crate::system::System::initialize). /// /// A system then can be executed using [`System::run_unsafe`](crate::system::System::run_unsafe) with a `&World` and use methods with interior mutability to access resource values. /// @@ -998,7 +998,9 @@ impl<'w> UnsafeEntityCell<'w> { /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the queried data immutably /// - no mutable references to the queried data exist at the same time - pub(crate) unsafe fn get_components(&self) -> Option> { + pub(crate) unsafe fn get_components( + &self, + ) -> Option> { // SAFETY: World is only used to access query data and initialize query state let state = unsafe { let world = self.world().world(); @@ -1028,7 +1030,8 @@ impl<'w> UnsafeEntityCell<'w> { // Table corresponds to archetype. State is the same state used to init fetch above. unsafe { Q::set_archetype(&mut fetch, &state, archetype, table) } // SAFETY: Called after set_archetype above. Entity and location are guaranteed to exist. - unsafe { Some(Q::fetch(&mut fetch, self.id(), location.table_row)) } + let item = unsafe { Q::fetch(&state, &mut fetch, self.id(), location.table_row) }; + Some(Q::release_state(item)) } else { None } diff --git a/crates/bevy_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index 60a6515fd8..9e05bc7a85 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_encase_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy derive macro for encase" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.17.0-dev" } encase_derive_impl = "0.10" [lints] diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index 0afb6babbf..afb20318bc 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gilrs" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Gamepad system made using Gilrs for Bevy Engine" homepage = "https://bevy.org" @@ -10,12 +10,12 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index db1b404abc..7ec1c2e93b 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -39,7 +39,7 @@ thread_local! { /// `NonSendMut` parameter, which told Bevy that the system was `!Send`, but now with the removal of `!Send` /// resource/system parameter usage, there is no internal guarantee that the system will run in only one thread, so /// we need to rely on the platform to make such a guarantee. - static GILRS: RefCell> = const { RefCell::new(None) }; + pub static GILRS: RefCell> = const { RefCell::new(None) }; } #[derive(Resource)] @@ -47,6 +47,7 @@ pub(crate) struct Gilrs { #[cfg(not(target_arch = "wasm32"))] cell: SyncCell, } + impl Gilrs { #[inline] pub fn with(&mut self, f: impl FnOnce(&mut gilrs::Gilrs)) { diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 97a41f15b6..c4833dbe7e 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gizmos" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides gizmos for Bevy Engine" homepage = "https://bevy.org" @@ -15,21 +15,21 @@ bevy_render = ["dep:bevy_render", "bevy_core_pipeline"] [dependencies] # Bevy -bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev", optional = true } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev", optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_gizmos_macros = { path = "macros", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev", optional = true } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev", optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_gizmos_macros = { path = "macros", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } # other bytemuck = "1.0" diff --git a/crates/bevy_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml index e15d0367b2..b7effe24b0 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gizmos_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_gizmos" homepage = "https://bevy.org" @@ -13,7 +13,7 @@ proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 87af7c4925..e0f139a1a8 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -9,7 +9,8 @@ use core::{ use bevy_color::{Color, LinearRgba}; use bevy_ecs::{ - component::Tick, + component::{ComponentId, Tick}, + query::FilteredAccessSet, resource::Resource, system::{ Deferred, ReadOnlySystemParam, Res, SystemBuffer, SystemMeta, SystemParam, @@ -199,12 +200,26 @@ where type State = GizmosFetchState; type Item<'w, 's> = Gizmos<'w, 's, Config, Clear>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { GizmosFetchState { - state: GizmosState::::init_state(world, system_meta), + state: GizmosState::::init_state(world), } } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + GizmosState::::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); + } + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { GizmosState::::apply(&mut state.state, system_meta, world); } diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index cdcfc41236..2c85a0859d 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -172,6 +172,7 @@ where ); } } + impl GizmoBuffer where Config: GizmoConfigGroup, diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index de48d94e42..f9512bc7ab 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -554,6 +554,7 @@ impl RenderAsset for GpuLineGizmo { gizmo: Self::SourceAsset, _: AssetId, render_device: &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let list_position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::VERTEX, @@ -631,8 +632,8 @@ impl RenderCommand

for SetLineGizmoBindGroup #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - uniform_index: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + uniform_index: Option>, bind_group: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -662,8 +663,8 @@ impl RenderCommand

for DrawLineGizmo #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -725,8 +726,8 @@ impl RenderCommand

for DrawLineJointGizmo { #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 1cc70c67cb..69854f7de4 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -123,8 +123,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { .mesh_pipeline .get_view_layout(key.view_key.into()) .clone(); - - let layout = vec![view_layout, self.uniform_layout.clone()]; + let layout = vec![view_layout.main_layout.clone(), self.uniform_layout.clone()]; let fragment_entry_point = match key.line_style { GizmoLineStyle::Solid => "fragment_solid", @@ -220,8 +219,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { .mesh_pipeline .get_view_layout(key.view_key.into()) .clone(); - - let layout = vec![view_layout, self.uniform_layout.clone()]; + let layout = vec![view_layout.main_layout.clone(), self.uniform_layout.clone()]; if key.joints == GizmoLineJoint::None { error!("There is no entry point for line joints with GizmoLineJoints::None. Please consider aborting the drawing process before reaching this stage."); diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 05b16d1fc9..c46b74b7ca 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gltf" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy Engine GLTF loading" homepage = "https://bevy.org" @@ -18,25 +18,25 @@ pbr_specular_textures = ["bevy_pbr/pbr_specular_textures"] [dependencies] # bevy -bevy_animation = { path = "../bevy_animation", version = "0.16.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_scene = { path = "../bevy_scene", version = "0.16.0-dev", features = [ +bevy_animation = { path = "../bevy_animation", version = "0.17.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_scene = { path = "../bevy_scene", version = "0.17.0-dev", features = [ "bevy_render", ] } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -66,7 +66,7 @@ smallvec = "1.11" tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } [lints] workspace = true diff --git a/crates/bevy_gltf/src/convert_coordinates.rs b/crates/bevy_gltf/src/convert_coordinates.rs new file mode 100644 index 0000000000..4148cecd9a --- /dev/null +++ b/crates/bevy_gltf/src/convert_coordinates.rs @@ -0,0 +1,80 @@ +use core::f32::consts::PI; + +use bevy_math::{Mat4, Quat, Vec3}; +use bevy_transform::components::Transform; + +pub(crate) trait ConvertCoordinates { + /// Converts the glTF coordinates to Bevy's coordinate system. + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_coordinates(self) -> Self; +} + +pub(crate) trait ConvertCameraCoordinates { + /// Like `convert_coordinates`, but uses the following for the lens rotation: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_camera_coordinates(self) -> Self; +} + +impl ConvertCoordinates for Vec3 { + fn convert_coordinates(self) -> Self { + Vec3::new(-self.x, self.y, -self.z) + } +} + +impl ConvertCoordinates for [f32; 3] { + fn convert_coordinates(self) -> Self { + [-self[0], self[1], -self[2]] + } +} + +impl ConvertCoordinates for [f32; 4] { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + [-self[0], self[1], -self[2], self[3]] + } +} + +impl ConvertCoordinates for Quat { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + Quat::from_array([-self.x, self.y, -self.z, self.w]) + } +} + +impl ConvertCoordinates for Mat4 { + fn convert_coordinates(self) -> Self { + let m: Mat4 = Mat4::from_scale(Vec3::new(-1.0, 1.0, -1.0)); + // Same as the original matrix + let m_inv = m; + m_inv * self * m + } +} + +impl ConvertCoordinates for Transform { + fn convert_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotation = self.rotation.convert_coordinates(); + self + } +} + +impl ConvertCameraCoordinates for Transform { + fn convert_camera_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotate_y(PI); + self + } +} diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 97600f3ed0..bbcb13a908 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -91,6 +91,7 @@ //! You can use [`GltfAssetLabel`] to ensure you are using the correct label. mod assets; +mod convert_coordinates; mod label; mod loader; mod vertex_attributes; @@ -155,8 +156,24 @@ impl DefaultGltfImageSampler { pub struct GltfPlugin { /// The default image sampler to lay glTF sampler data on top of. /// - /// Can be modified with [`DefaultGltfImageSampler`] resource. + /// Can be modified with the [`DefaultGltfImageSampler`] resource. pub default_sampler: ImageSamplerDescriptor, + + /// Whether to convert glTF coordinates to Bevy's coordinate system by default. + /// If set to `true`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system + /// such that objects looking forward in glTF will also look forward in Bevy. + /// + /// The exact coordinate system conversion is as follows: + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + pub convert_coordinates: bool, + /// Registry for custom vertex attributes. /// /// To specify, use [`GltfPlugin::add_custom_vertex_attribute`]. @@ -168,6 +185,7 @@ impl Default for GltfPlugin { GltfPlugin { default_sampler: ImageSamplerDescriptor::linear(), custom_vertex_attributes: HashMap::default(), + convert_coordinates: false, } } } @@ -218,10 +236,12 @@ impl Plugin for GltfPlugin { let default_sampler_resource = DefaultGltfImageSampler::new(&self.default_sampler); let default_sampler = default_sampler_resource.get_internal(); app.insert_resource(default_sampler_resource); + app.register_asset_loader(GltfLoader { supported_compressed_formats, custom_vertex_attributes: self.custom_vertex_attributes.clone(), default_sampler, + default_convert_coordinates: self.convert_coordinates, }); } } diff --git a/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs b/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs index 60a153fed3..25d6677832 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs @@ -11,7 +11,7 @@ pub(crate) fn primitive_name(mesh: &Mesh<'_>, material: &Material) -> String { let mesh_name = mesh.name().unwrap_or("Mesh"); if let Some(material_name) = material.name() { - format!("{}.{}", mesh_name, material_name) + format!("{mesh_name}.{material_name}") } else { mesh_name.to_string() } diff --git a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs index 83e6778b99..3fce51d527 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs @@ -10,7 +10,10 @@ use itertools::Itertools; #[cfg(feature = "bevy_animation")] use bevy_platform::collections::{HashMap, HashSet}; -use crate::GltfError; +use crate::{ + convert_coordinates::{ConvertCameraCoordinates as _, ConvertCoordinates as _}, + GltfError, +}; pub(crate) fn node_name(node: &Node) -> Name { let name = node @@ -26,8 +29,8 @@ pub(crate) fn node_name(node: &Node) -> Name { /// on [`Node::transform()`](gltf::Node::transform) directly because it uses optimized glam types and /// if `libm` feature of `bevy_math` crate is enabled also handles cross /// platform determinism properly. -pub(crate) fn node_transform(node: &Node) -> Transform { - match node.transform() { +pub(crate) fn node_transform(node: &Node, convert_coordinates: bool) -> Transform { + let transform = match node.transform() { gltf::scene::Transform::Matrix { matrix } => { Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) } @@ -40,6 +43,15 @@ pub(crate) fn node_transform(node: &Node) -> Transform { rotation: bevy_math::Quat::from_array(rotation), scale: Vec3::from(scale), }, + }; + if convert_coordinates { + if node.camera().is_some() { + transform.convert_camera_coordinates() + } else { + transform.convert_coordinates() + } + } else { + transform } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index abc555002a..a326af0526 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -84,6 +84,7 @@ use self::{ texture::{texture_handle, texture_sampler, texture_transform_to_affine2}, }, }; +use crate::convert_coordinates::ConvertCoordinates as _; /// An error that occurs when loading a glTF file. #[derive(Error, Debug)] @@ -150,6 +151,20 @@ pub struct GltfLoader { pub custom_vertex_attributes: HashMap, MeshVertexAttribute>, /// Arc to default [`ImageSamplerDescriptor`]. pub default_sampler: Arc>, + /// Whether to convert glTF coordinates to Bevy's coordinate system by default. + /// If set to `true`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system + /// such that objects looking forward in glTF will also look forward in Bevy. + /// + /// The exact coordinate system conversion is as follows: + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + pub default_convert_coordinates: bool, } /// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of @@ -187,10 +202,27 @@ pub struct GltfLoaderSettings { pub include_source: bool, /// Overrides the default sampler. Data from sampler node is added on top of that. /// - /// If None, uses global default which is stored in `DefaultGltfImageSampler` resource. + /// If None, uses the global default which is stored in the [`DefaultGltfImageSampler`](crate::DefaultGltfImageSampler) resource. pub default_sampler: Option, /// If true, the loader will ignore sampler data from gltf and use the default sampler. pub override_sampler: bool, + /// Overrides the default glTF coordinate conversion setting. + /// + /// If set to `Some(true)`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system + /// such that objects looking forward in glTF will also look forward in Bevy. + /// + /// The exact coordinate system conversion is as follows: + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// If `None`, uses the global default set by [`GltfPlugin::convert_coordinates`](crate::GltfPlugin::convert_coordinates). + pub convert_coordinates: Option, } impl Default for GltfLoaderSettings { @@ -203,6 +235,7 @@ impl Default for GltfLoaderSettings { include_source: false, default_sampler: None, override_sampler: false, + convert_coordinates: None, } } } @@ -262,6 +295,11 @@ async fn load_gltf<'a, 'b, 'c>( paths }; + let convert_coordinates = match settings.convert_coordinates { + Some(convert_coordinates) => convert_coordinates, + None => loader.default_convert_coordinates, + }; + #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { use bevy_animation::{animated_field, animation_curves::*, gltf_curves::*, VariableCurve}; @@ -303,7 +341,16 @@ async fn load_gltf<'a, 'b, 'c>( match outputs { ReadOutputs::Translations(tr) => { let translation_property = animated_field!(Transform::translation); - let translations: Vec = tr.map(Vec3::from).collect(); + let translations: Vec = tr + .map(Vec3::from) + .map(|verts| { + if convert_coordinates { + Vec3::convert_coordinates(verts) + } else { + verts + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( translation_property, @@ -350,8 +397,17 @@ async fn load_gltf<'a, 'b, 'c>( } ReadOutputs::Rotations(rots) => { let rotation_property = animated_field!(Transform::rotation); - let rotations: Vec = - rots.into_f32().map(Quat::from_array).collect(); + let rotations: Vec = rots + .into_f32() + .map(Quat::from_array) + .map(|quat| { + if convert_coordinates { + Quat::convert_coordinates(quat) + } else { + quat + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( rotation_property, @@ -633,6 +689,7 @@ async fn load_gltf<'a, 'b, 'c>( accessor, &buffer_data, &loader.custom_vertex_attributes, + convert_coordinates, ) { Ok((attribute, values)) => mesh.insert_attribute(attribute, values), Err(err) => warn!("{}", err), @@ -752,7 +809,17 @@ async fn load_gltf<'a, 'b, 'c>( let reader = gltf_skin.reader(|buffer| Some(&buffer_data[buffer.index()])); let local_to_bone_bind_matrices: Vec = reader .read_inverse_bind_matrices() - .map(|mats| mats.map(|mat| Mat4::from_cols_array_2d(&mat)).collect()) + .map(|mats| { + mats.map(|mat| Mat4::from_cols_array_2d(&mat)) + .map(|mat| { + if convert_coordinates { + mat.convert_coordinates() + } else { + mat + } + }) + .collect() + }) .unwrap_or_else(|| { core::iter::repeat_n(Mat4::IDENTITY, gltf_skin.joints().len()).collect() }); @@ -834,7 +901,7 @@ async fn load_gltf<'a, 'b, 'c>( &node, children, mesh, - node_transform(&node), + node_transform(&node, convert_coordinates), skin, node.extras().as_deref().map(GltfExtras::from), ); @@ -885,6 +952,7 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] None, &gltf.document, + convert_coordinates, ); if result.is_err() { err = Some(result); @@ -1304,9 +1372,10 @@ fn load_node( #[cfg(feature = "bevy_animation")] animation_roots: &HashSet, #[cfg(feature = "bevy_animation")] mut animation_context: Option, document: &Document, + convert_coordinates: bool, ) -> Result<(), GltfError> { let mut gltf_error = None; - let transform = node_transform(gltf_node); + let transform = node_transform(gltf_node, convert_coordinates); let world_transform = *parent_transform * transform; // according to https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#instantiation, // if the determinant of the transform is negative we must invert the winding order of @@ -1359,7 +1428,6 @@ fn load_node( }, ..OrthographicProjection::default_3d() }; - Projection::Orthographic(orthographic_projection) } gltf::camera::Projection::Perspective(perspective) => { @@ -1377,6 +1445,7 @@ fn load_node( Projection::Perspective(perspective_projection) } }; + node.insert(( Camera3d::default(), projection, @@ -1575,6 +1644,7 @@ fn load_node( #[cfg(feature = "bevy_animation")] animation_context.clone(), document, + convert_coordinates, ) { gltf_error = Some(err); return; diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs index d4ae811c90..5d6ce9eb3e 100644 --- a/crates/bevy_gltf/src/vertex_attributes.rs +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -6,6 +6,8 @@ use gltf::{ }; use thiserror::Error; +use crate::convert_coordinates::ConvertCoordinates; + /// Represents whether integer data requires normalization #[derive(Copy, Clone)] struct Normalization(bool); @@ -132,15 +134,32 @@ impl<'a> VertexAttributeIter<'a> { } /// Materializes values for any supported format of vertex attribute - fn into_any_values(self) -> Result { + fn into_any_values(self, convert_coordinates: bool) -> Result { 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::F32x3(it) => Ok(if convert_coordinates { + // The following f32x3 values need to be converted to the correct coordinate system + // - Positions + // - Normals + // + // See + Values::Float32x3(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + Values::Float32x3(it.collect()) + }), VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())), - VertexAttributeIter::F32x4(it) => Ok(Values::Float32x4(it.collect())), + VertexAttributeIter::F32x4(it) => Ok(if convert_coordinates { + // The following f32x4 values need to be converted to the correct coordinate system + // - Tangents + // + // See + Values::Float32x4(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + 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)) @@ -188,7 +207,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => Ok(Values::Float32x4( ReadColors::RgbaU16(it).into_rgba_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -198,7 +217,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U8x4(it, Normalization(false)) => { Ok(Values::Uint16x4(ReadJoints::U8(it).into_u16().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -211,7 +230,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => { Ok(Values::Float32x4(ReadWeights::U16(it).into_f32().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -224,7 +243,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x2(it, Normalization(true)) => Ok(Values::Float32x2( ReadTexCoords::U16(it).into_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } } @@ -252,28 +271,49 @@ pub(crate) fn convert_attribute( accessor: gltf::Accessor, buffer_data: &Vec>, custom_vertex_attributes: &HashMap, MeshVertexAttribute>, + convert_coordinates: bool, ) -> 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::TexCoords(1) => Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord)), - gltf::Semantic::Joints(0) => { - Some((Mesh::ATTRIBUTE_JOINT_INDEX, ConversionMode::JointIndex)) + if let Some((attribute, conversion, convert_coordinates)) = match &semantic { + gltf::Semantic::Positions => Some(( + Mesh::ATTRIBUTE_POSITION, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Normals => Some(( + Mesh::ATTRIBUTE_NORMAL, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Tangents => Some(( + Mesh::ATTRIBUTE_TANGENT, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba, false)), + gltf::Semantic::TexCoords(0) => { + Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord, false)) } - gltf::Semantic::Weights(0) => { - Some((Mesh::ATTRIBUTE_JOINT_WEIGHT, ConversionMode::JointWeight)) + gltf::Semantic::TexCoords(1) => { + Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord, false)) } + gltf::Semantic::Joints(0) => Some(( + Mesh::ATTRIBUTE_JOINT_INDEX, + ConversionMode::JointIndex, + false, + )), + gltf::Semantic::Weights(0) => Some(( + Mesh::ATTRIBUTE_JOINT_WEIGHT, + ConversionMode::JointWeight, + false, + )), gltf::Semantic::Extras(name) => custom_vertex_attributes .get(name.as_str()) - .map(|attr| (*attr, ConversionMode::Any)), + .map(|attr| (*attr, ConversionMode::Any, false)), _ => 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::Any => iter.into_any_values(convert_coordinates), ConversionMode::Rgba => iter.into_rgba_values(), ConversionMode::TexCoord => iter.into_tex_coord_values(), ConversionMode::JointIndex => iter.into_joint_index_values(), diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index 7f8128e365..1ed4f29ca6 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_image" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides image types for Bevy Engine" homepage = "https://bevy.org" @@ -38,19 +38,22 @@ serialize = ["bevy_reflect", "bevy_platform/serialize"] zlib = ["flate2"] zstd = ["ruzstd"] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = ["basis-universal"] + [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ "serialize", "wgpu-types", ] } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -60,7 +63,7 @@ image = { version = "0.25.2", default-features = false } # misc bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5" } -wgpu-types = { version = "24", default-features = false } +wgpu-types = { version = "25", default-features = false } serde = { version = "1", features = ["derive"] } thiserror = { version = "2", default-features = false } futures-lite = "2.0.1" @@ -77,7 +80,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } half = { version = "2.4.1" } [dev-dependencies] -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } [lints] workspace = true diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index e7548bb2bd..c92f947c5f 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -356,6 +356,8 @@ pub struct Image { pub sampler: ImageSampler, pub texture_view_descriptor: Option>>, pub asset_usage: RenderAssetUsages, + /// Whether this image should be copied on the GPU when resized. + pub copy_on_resize: bool, } /// Used in [`Image`], this determines what image sampler to use when rendering. The default setting, @@ -747,12 +749,15 @@ impl Image { label: None, mip_level_count: 1, sample_count: 1, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::COPY_SRC, view_formats: &[], }, sampler: ImageSampler::Default, texture_view_descriptor: None, asset_usage, + copy_on_resize: false, } } @@ -813,8 +818,7 @@ impl Image { ); debug_assert!( pixel.len() <= byte_len, - "Fill data must fit within pixel buffer (expected {}B).", - byte_len, + "Fill data must fit within pixel buffer (expected {byte_len}B).", ); let data = pixel.iter().copied().cycle().take(byte_len).collect(); Image::new(size, dimension, data, format, asset_usage) @@ -887,13 +891,15 @@ impl Image { /// When growing, the new space is filled with 0. When shrinking, the image is clipped. /// /// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`]. - pub fn resize_in_place(&mut self, new_size: Extent3d) -> Result<(), ResizeError> { + pub fn resize_in_place(&mut self, new_size: Extent3d) { let old_size = self.texture_descriptor.size; let pixel_size = self.texture_descriptor.format.pixel_size(); let byte_len = self.texture_descriptor.format.pixel_size() * new_size.volume(); + self.texture_descriptor.size = new_size; let Some(ref mut data) = self.data else { - return Err(ResizeError::ImageWithoutData); + self.copy_on_resize = true; + return; }; let mut new: Vec = vec![0; byte_len]; @@ -923,10 +929,6 @@ impl Image { } self.data = Some(new); - - self.texture_descriptor.size = new_size; - - Ok(()) } /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets @@ -1591,14 +1593,6 @@ pub enum TextureError { IncompleteCubemap, } -/// An error that occurs when an image cannot be resized. -#[derive(Error, Debug)] -pub enum ResizeError { - /// Failed to resize an Image because it has no data. - #[error("resize method requires cpu-side image data but none was present")] - ImageWithoutData, -} - /// The type of a raw image buffer. #[derive(Debug)] pub enum ImageType<'a> { @@ -1822,13 +1816,11 @@ mod test { } // Grow image - image - .resize_in_place(Extent3d { - width: 4, - height: 4, - depth_or_array_layers: 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 4, + height: 4, + depth_or_array_layers: 1, + }); // After growing, the test pattern should be the same. assert!(matches!( @@ -1849,13 +1841,11 @@ mod test { )); // Shrink - image - .resize_in_place(Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }); // Images outside of the new dimensions should be clipped assert!(image.get_color_at(1, 1).is_err()); @@ -1898,13 +1888,11 @@ mod test { } // Grow image - image - .resize_in_place(Extent3d { - width: 4, - height: 4, - depth_or_array_layers: LAYERS + 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 4, + height: 4, + depth_or_array_layers: LAYERS + 1, + }); // After growing, the test pattern should be the same. assert!(matches!( @@ -1929,13 +1917,11 @@ mod test { } // Shrink - image - .resize_in_place(Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }); // Images outside of the new dimensions should be clipped assert!(image.get_color_at_3d(1, 1, 0).is_err()); @@ -1944,13 +1930,11 @@ mod test { assert!(image.get_color_at_3d(0, 0, 1).is_err()); // Grow layers - image - .resize_in_place(Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 2, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 2, + }); // Pixels in the newly added layer should be zeroes. assert!(matches!( diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index 55f74a5f14..02c3785ced 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -14,7 +14,7 @@ mod image; pub use self::image::*; #[cfg(feature = "basis-universal")] mod basis; -#[cfg(feature = "basis-universal")] +#[cfg(feature = "compressed_image_saver")] mod compressed_image_saver; #[cfg(feature = "dds")] mod dds; @@ -29,7 +29,7 @@ mod ktx2; mod texture_atlas; mod texture_atlas_builder; -#[cfg(feature = "basis-universal")] +#[cfg(feature = "compressed_image_saver")] pub use compressed_image_saver::*; #[cfg(feature = "dds")] pub use dds::*; diff --git a/crates/bevy_image/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs index 4caeed8c07..67e1b20317 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -34,6 +34,7 @@ pub struct TextureAtlasSources { /// Maps from a specific image handle to the index in `textures` where they can be found. pub texture_ids: HashMap, usize>, } + impl TextureAtlasSources { /// Retrieves the texture *section* index of the given `texture` handle. pub fn texture_index(&self, texture: impl Into>) -> Option { diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index 6b805b83bf..7c69aad54a 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_input" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides input functionality for Bevy Engine" homepage = "https://bevy.org" @@ -60,14 +60,14 @@ libm = ["bevy_math/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "glam", ], default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1", features = [ @@ -75,7 +75,7 @@ serde = { version = "1", features = [ "derive", ], default-features = false, optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } smol_str = { version = "0.2", default-features = false, optional = true } log = { version = "0.4", default-features = false } diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 58ab62aefa..e4ff47f470 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -122,7 +122,7 @@ use { /// [`DetectChangesMut::bypass_change_detection`]: bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection #[derive(Debug, Clone, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default, Resource))] -pub struct ButtonInput { +pub struct ButtonInput { /// A collection of every button that is currently being pressed. pressed: HashSet, /// A collection of every button that has just been pressed. @@ -131,7 +131,7 @@ pub struct ButtonInput { just_released: HashSet, } -impl Default for ButtonInput { +impl Default for ButtonInput { fn default() -> Self { Self { pressed: Default::default(), @@ -143,12 +143,12 @@ impl Default for ButtonInput { impl ButtonInput where - T: Copy + Eq + Hash + Send + Sync + 'static, + T: Clone + Eq + Hash + Send + Sync + 'static, { /// Registers a press for the given `input`. pub fn press(&mut self, input: T) { // Returns `true` if the `input` wasn't pressed. - if self.pressed.insert(input) { + if self.pressed.insert(input.clone()) { self.just_pressed.insert(input); } } diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 2b0148909c..a448ec3d48 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ change_detection::DetectChangesMut, component::Component, entity::Entity, - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, name::Name, system::{Commands, Query}, }; @@ -32,7 +32,7 @@ use thiserror::Error; /// the in-frame relative ordering of events is important. /// /// This event is produced by `bevy_input`. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -59,7 +59,7 @@ pub enum GamepadEvent { /// the in-frame relative ordering of events is important. /// /// This event type is used by `bevy_input` to feed its components. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -80,7 +80,7 @@ pub enum RawGamepadEvent { } /// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -112,7 +112,7 @@ impl RawGamepadButtonChangedEvent { } /// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -143,9 +143,9 @@ impl RawGamepadAxisChangedEvent { } } -/// A Gamepad connection event. Created when a connection to a gamepad +/// A [`Gamepad`] connection event. Created when a connection to a gamepad /// is established and when a gamepad is disconnected. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -184,7 +184,7 @@ impl GamepadConnectionEvent { } /// [`GamepadButton`] event triggered by a digital state change. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -216,7 +216,7 @@ impl GamepadButtonStateChangedEvent { } /// [`GamepadButton`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -251,7 +251,7 @@ impl GamepadButtonChangedEvent { } /// [`GamepadAxis`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( feature = "bevy_reflect", @@ -1516,7 +1516,7 @@ pub fn gamepad_connection_system( product_id, } => { let Ok(mut gamepad) = commands.get_entity(id) else { - warn!("Gamepad {} removed before handling connection event.", id); + warn!("Gamepad {id} removed before handling connection event."); continue; }; gamepad.insert(( @@ -1527,18 +1527,18 @@ pub fn gamepad_connection_system( ..Default::default() }, )); - info!("Gamepad {} connected.", id); + info!("Gamepad {id} connected."); } GamepadConnection::Disconnected => { let Ok(mut gamepad) = commands.get_entity(id) else { - warn!("Gamepad {} removed before handling disconnection event. You can ignore this if you manually removed it.", id); + warn!("Gamepad {id} removed before handling disconnection event. You can ignore this if you manually removed it."); continue; }; // Gamepad entities are left alive to preserve their state (e.g. [`GamepadSettings`]). // Instead of despawning, we remove Gamepad components that don't need to preserve state // and re-add them if they ever reconnect. gamepad.remove::(); - info!("Gamepad {} disconnected.", id); + info!("Gamepad {id} disconnected."); } } } @@ -1774,7 +1774,7 @@ impl GamepadRumbleIntensity { #[doc(alias = "force feedback")] #[doc(alias = "vibration")] #[doc(alias = "vibrate")] -#[derive(Event, Clone)] +#[derive(Event, BufferedEvent, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone))] pub enum GamepadRumbleRequest { /// Add a rumble to the given gamepad. diff --git a/crates/bevy_input/src/gestures.rs b/crates/bevy_input/src/gestures.rs index 5cd14d4634..9daa21d525 100644 --- a/crates/bevy_input/src/gestures.rs +++ b/crates/bevy_input/src/gestures.rs @@ -1,6 +1,6 @@ //! Gestures functionality, from touchscreens and touchpads. -use bevy_ecs::event::Event; +use bevy_ecs::event::{BufferedEvent, Event}; use bevy_math::Vec2; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -17,7 +17,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -39,7 +39,7 @@ pub struct PinchGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -58,7 +58,7 @@ pub struct RotationGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -76,7 +76,7 @@ pub struct DoubleTapGesture; /// ## Platform-specific /// /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index ea5452fb53..70efe18a84 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -69,7 +69,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, system::ResMut, }; @@ -92,9 +92,10 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// ## Usage /// -/// The event is consumed inside of the [`keyboard_input_system`] -/// to update the [`ButtonInput`](ButtonInput) resource. -#[derive(Event, Debug, Clone, PartialEq, Eq, Hash)] +/// The event is consumed inside of the [`keyboard_input_system`] to update the +/// [`ButtonInput`](ButtonInput) and +/// [`ButtonInput`](ButtonInput) resources. +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -107,8 +108,12 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; )] pub struct KeyboardInput { /// The physical key code of the key. + /// + /// This corresponds to the location of the key independent of the keyboard layout. pub key_code: KeyCode, - /// The logical key of the input + /// The logical key of the input. + /// + /// This corresponds to the actual key taking keyboard layout into account. pub logical_key: Key, /// The press state of the key. pub state: ButtonState, @@ -139,7 +144,7 @@ pub struct KeyboardInput { /// when, for example, switching between windows with 'Alt-Tab' or using any other /// OS specific key combination that leads to Bevy window losing focus and not receiving any /// input events -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -148,32 +153,46 @@ pub struct KeyboardInput { )] pub struct KeyboardFocusLost; -/// Updates the [`ButtonInput`] resource with the latest [`KeyboardInput`] events. +/// Updates the [`ButtonInput`] and [`ButtonInput`] resources with the latest [`KeyboardInput`] events. /// /// ## Differences /// -/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources is that +/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources are that /// the latter has convenient functions such as [`ButtonInput::pressed`], [`ButtonInput::just_pressed`] and [`ButtonInput::just_released`] and is window id agnostic. +/// +/// There is a [`ButtonInput`] for both [`KeyCode`] and [`Key`] as they are both useful in different situations, see their documentation for the details. pub fn keyboard_input_system( - mut key_input: ResMut>, + mut keycode_input: ResMut>, + mut key_input: ResMut>, mut keyboard_input_events: EventReader, mut focus_events: EventReader, ) { - // Avoid clearing if it's not empty to ensure change detection is not triggered. + // Avoid clearing if not empty to ensure change detection is not triggered. + keycode_input.bypass_change_detection().clear(); key_input.bypass_change_detection().clear(); + for event in keyboard_input_events.read() { let KeyboardInput { - key_code, state, .. + key_code, + logical_key, + state, + .. } = event; match state { - ButtonState::Pressed => key_input.press(*key_code), - ButtonState::Released => key_input.release(*key_code), + ButtonState::Pressed => { + keycode_input.press(*key_code); + key_input.press(logical_key.clone()); + } + ButtonState::Released => { + keycode_input.release(*key_code); + key_input.release(logical_key.clone()); + } } } // Release all cached input to avoid having stuck input when switching between windows in os if !focus_events.is_empty() { - key_input.release_all(); + keycode_input.release_all(); focus_events.clear(); } } @@ -220,13 +239,13 @@ pub enum NativeKeyCode { /// It is used as the generic `T` value of an [`ButtonInput`] to create a `Res>`. /// /// Code representing the location of a physical key -/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// This mostly conforms to the [`UI Events Specification's KeyboardEvent.code`] with a few /// exceptions: /// - The keys that the specification calls `MetaLeft` and `MetaRight` are named `SuperLeft` and /// `SuperRight` here. /// - The key that the specification calls "Super" is reported as `Unidentified` here. /// -/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +/// [`UI Events Specification's KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables /// /// ## Updating /// @@ -756,6 +775,19 @@ pub enum NativeKey { /// The logical key code of a [`KeyboardInput`]. /// +/// This contains the actual value that is produced by pressing the key. This is +/// useful when you need the actual letters, and for symbols like `+` and `-` +/// when implementing zoom, as they can be in different locations depending on +/// the keyboard layout. +/// +/// In many cases you want the key location instead, for example when +/// implementing WASD controls so the keys are located the same place on QWERTY +/// and other layouts. In that case use [`KeyCode`] instead. +/// +/// ## Usage +/// +/// It is used as the generic `T` value of an [`ButtonInput`] to create a `Res>`. +/// /// ## Technical /// /// Its values map 1 to 1 to winit's Key. diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 653af4c991..5a6a177223 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -49,7 +49,7 @@ use bevy_ecs::prelude::*; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use gestures::*; -use keyboard::{keyboard_input_system, KeyCode, KeyboardFocusLost, KeyboardInput}; +use keyboard::{keyboard_input_system, Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use mouse::{ accumulate_mouse_motion_system, accumulate_mouse_scroll_system, mouse_button_input_system, AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton, MouseButtonInput, MouseMotion, @@ -89,6 +89,7 @@ impl Plugin for InputPlugin { .add_event::() .add_event::() .init_resource::>() + .init_resource::>() .add_systems(PreUpdate, keyboard_input_system.in_set(InputSystems)) // mouse .add_event::() diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index 3a377d9329..e6b52bf51d 100644 --- a/crates/bevy_input/src/mouse.rs +++ b/crates/bevy_input/src/mouse.rs @@ -4,7 +4,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -26,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// The event is read inside of the [`mouse_button_input_system`] /// to update the [`ButtonInput`] resource. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -91,7 +91,7 @@ pub enum MouseButton { /// However, the event data does not make it possible to distinguish which device it is referring to. /// /// [`DeviceEvent::MouseMotion`]: https://docs.rs/winit/latest/winit/event/enum.DeviceEvent.html#variant.MouseMotion -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -140,7 +140,7 @@ pub enum MouseScrollUnit { /// A mouse wheel event. /// /// This event is the translated version of the `WindowEvent::MouseWheel` from the `winit` crate. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/touch.rs b/crates/bevy_input/src/touch.rs index 28f3159d53..df1cf3764f 100644 --- a/crates/bevy_input/src/touch.rs +++ b/crates/bevy_input/src/touch.rs @@ -2,7 +2,7 @@ use bevy_ecs::{ entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -37,7 +37,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// This event is the translated version of the `WindowEvent::Touch` from the `winit` crate. /// It is available to the end user and can be used for game logic. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index e7ff3f6fe8..60b824258d 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_input_focus" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Keyboard focus management" homepage = "https://bevy.org" @@ -60,12 +60,13 @@ libm = ["bevy_math/libm", "bevy_window/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", default-features = false } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "glam", ], default-features = false, optional = true } @@ -73,9 +74,6 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ thiserror = { version = "2", default-features = false } log = { version = "0.4", default-features = false } -[dev-dependencies] -smol_str = "0.2" - [lints] workspace = true diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 436e838fb1..df7690ef26 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -30,7 +30,7 @@ pub mod tab_navigation; mod autofocus; pub use autofocus::*; -use bevy_app::{App, Plugin, PreUpdate, Startup}; +use bevy_app::{App, Plugin, PostStartup, PreUpdate}; use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal}; use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel}; use bevy_window::{PrimaryWindow, Window}; @@ -137,19 +137,23 @@ pub struct InputFocusVisible(pub bool); /// /// To set up your own bubbling input event, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, /// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`]. -#[derive(Clone, Debug, Component)] +#[derive(Event, EntityEvent, Clone, Debug, Component)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] -pub struct FocusedInput { +pub struct FocusedInput { /// The underlying input event. pub input: E, /// The primary window entity. window: Entity, } -impl Event for FocusedInput { - type Traversal = WindowTraversal; - - const AUTO_PROPAGATE: bool = true; +/// An event which is used to set input focus. Trigger this on an entity, and it will bubble +/// until it finds a focusable entity, and then set focus to it. +#[derive(Clone, Event, EntityEvent)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] +pub struct AcquireFocus { + /// The primary window entity. + window: Entity, } #[derive(QueryData)] @@ -159,8 +163,26 @@ pub struct WindowTraversal { window: Option<&'static Window>, } -impl Traversal> for WindowTraversal { - fn traverse(item: Self::Item<'_>, event: &FocusedInput) -> Option { +impl Traversal> for WindowTraversal { + fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput) -> Option { + let WindowTraversalItem { child_of, window } = item; + + // Send event to parent, if it has one. + if let Some(child_of) = child_of { + return Some(child_of.parent()); + }; + + // Otherwise, send it to the window entity (unless this is a window entity). + if window.is_none() { + return Some(event.window); + } + + None + } +} + +impl Traversal for WindowTraversal { + fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option { let WindowTraversalItem { child_of, window } = item; // Send event to parent, if it has one. @@ -185,7 +207,7 @@ pub struct InputDispatchPlugin; impl Plugin for InputDispatchPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, set_initial_focus) + app.add_systems(PostStartup, set_initial_focus) .init_resource::() .init_resource::() .add_systems( @@ -218,17 +240,19 @@ pub enum InputFocusSystems { #[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")] pub type InputFocusSet = InputFocusSystems; -/// Sets the initial focus to the primary window, if any. +/// If no entity is focused, sets the focus to the primary window, if any. pub fn set_initial_focus( mut input_focus: ResMut, window: Single>, ) { - input_focus.0 = Some(*window); + if input_focus.0.is_none() { + input_focus.0 = Some(*window); + } } /// System which dispatches bubbled input events to the focused entity, or to the primary window /// if no entity has focus. -pub fn dispatch_focused_input( +pub fn dispatch_focused_input( mut key_events: EventReader, focus: Res, windows: Query>, @@ -368,47 +392,37 @@ mod tests { use super::*; use alloc::string::String; - use bevy_ecs::{ - lifecycle::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, - }; + use bevy_app::Startup; + use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld}; use bevy_input::{ keyboard::{Key, KeyCode}, ButtonState, InputPlugin, }; - use bevy_window::WindowResolution; - use smol_str::SmolStr; - - #[derive(Component)] - #[component(on_add = set_focus_on_add)] - struct SetFocusOnAdd; - - fn set_focus_on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { - let mut input_focus = world.resource_mut::(); - input_focus.set(entity); - } #[derive(Component, Default)] struct GatherKeyboardEvents(String); fn gather_keyboard_events( - trigger: Trigger>, + trigger: On>, mut query: Query<&mut GatherKeyboardEvents>, ) { - if let Ok(mut gather) = query.get_mut(trigger.target().unwrap()) { + if let Ok(mut gather) = query.get_mut(trigger.target()) { if let Key::Character(c) = &trigger.input.logical_key { gather.0.push_str(c.as_str()); } } } - const KEY_A_EVENT: KeyboardInput = KeyboardInput { - key_code: KeyCode::KeyA, - logical_key: Key::Character(SmolStr::new_static("A")), - state: ButtonState::Pressed, - text: Some(SmolStr::new_static("A")), - repeat: false, - window: Entity::PLACEHOLDER, - }; + fn key_a_event() -> KeyboardInput { + KeyboardInput { + key_code: KeyCode::KeyA, + logical_key: Key::Character("A".into()), + state: ButtonState::Pressed, + text: Some("A".into()), + repeat: false, + window: Entity::PLACEHOLDER, + } + } #[test] fn test_no_panics_if_resource_missing() { @@ -438,6 +452,55 @@ mod tests { .unwrap(); } + #[test] + fn initial_focus_unset_if_no_primary_window() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + app.update(); + + assert_eq!(app.world().resource::().0, None); + } + + #[test] + fn initial_focus_set_to_primary_window() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + let entity_window = app + .world_mut() + .spawn((Window::default(), PrimaryWindow)) + .id(); + app.update(); + + assert_eq!(app.world().resource::().0, Some(entity_window)); + } + + #[test] + fn initial_focus_not_overridden() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + app.world_mut().spawn((Window::default(), PrimaryWindow)); + + app.add_systems(Startup, |mut commands: Commands| { + commands.spawn(AutoFocus); + }); + + app.update(); + + let autofocus_entity = app + .world_mut() + .query_filtered::>() + .single(app.world()) + .unwrap(); + + assert_eq!( + app.world().resource::().0, + Some(autofocus_entity) + ); + } + #[test] fn test_keyboard_events() { fn get_gathered(app: &App, entity: Entity) -> &str { @@ -454,18 +517,14 @@ mod tests { app.add_plugins((InputPlugin, InputDispatchPlugin)) .add_observer(gather_keyboard_events); - let window = Window { - resolution: WindowResolution::new(800., 600.), - ..Default::default() - }; - app.world_mut().spawn((window, PrimaryWindow)); + app.world_mut().spawn((Window::default(), PrimaryWindow)); // Run the world for a single frame to set up the initial focus app.update(); let entity_a = app .world_mut() - .spawn((GatherKeyboardEvents::default(), SetFocusOnAdd)) + .spawn((GatherKeyboardEvents::default(), AutoFocus)) .id(); let child_of_b = app @@ -487,7 +546,7 @@ mod tests { assert!(!app.world().is_focus_visible(child_of_b)); // entity_a should receive this event - app.world_mut().send_event(KEY_A_EVENT); + app.world_mut().send_event(key_a_event()); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); @@ -500,7 +559,7 @@ mod tests { assert!(!app.world().is_focus_visible(entity_a)); // This event should be lost - app.world_mut().send_event(KEY_A_EVENT); + app.world_mut().send_event(key_a_event()); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); @@ -520,7 +579,8 @@ mod tests { assert!(app.world().is_focus_within(entity_b)); // These events should be received by entity_b and child_of_b - app.world_mut().send_event_batch([KEY_A_EVENT; 4]); + app.world_mut() + .send_event_batch(core::iter::repeat_n(key_a_event(), 4)); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 60df130ae0..6a8a24772d 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -30,7 +30,7 @@ use bevy_ecs::{ component::Component, entity::Entity, hierarchy::{ChildOf, Children}, - observer::Trigger, + observer::On, query::{With, Without}, system::{Commands, Query, Res, ResMut, SystemParam}, }; @@ -38,11 +38,12 @@ use bevy_input::{ keyboard::{KeyCode, KeyboardInput}, ButtonInput, ButtonState, }; -use bevy_window::PrimaryWindow; +use bevy_picking::events::{Pointer, Press}; +use bevy_window::{PrimaryWindow, Window}; use log::warn; use thiserror::Error; -use crate::{FocusedInput, InputFocus, InputFocusVisible}; +use crate::{AcquireFocus, FocusedInput, InputFocus, InputFocusVisible}; #[cfg(feature = "bevy_reflect")] use { @@ -312,6 +313,31 @@ impl TabNavigation<'_, '_> { } } +/// Observer which sets focus to the nearest ancestor that has tab index, using bubbling. +pub(crate) fn acquire_focus( + mut ev: On, + focusable: Query<(), With>, + windows: Query<(), With>, + mut focus: ResMut, +) { + // If the entity has a TabIndex + if focusable.contains(ev.target()) { + // Stop and focus it + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0 != Some(ev.target()) { + focus.0 = Some(ev.target()); + } + } else if windows.contains(ev.target()) { + // Stop and clear focus + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0.is_some() { + focus.clear(); + } + } +} + /// Plugin for navigating between focusable entities using keyboard input. pub struct TabNavigationPlugin; @@ -321,6 +347,8 @@ impl Plugin for TabNavigationPlugin { #[cfg(feature = "bevy_reflect")] app.register_type::().register_type::(); + app.add_observer(acquire_focus); + app.add_observer(click_to_focus); } } @@ -330,6 +358,30 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut focus_visible: ResMut, + windows: Query>, + mut commands: Commands, +) { + // Because `Pointer` is a bubbling event, we don't want to trigger an `AcquireFocus` event + // for every ancestor, but only for the original entity. Also, users may want to stop + // propagation on the pointer event at some point along the bubbling chain, so we need our + // own dedicated event whose propagation we can control. + if ev.target() == ev.original_target() { + // Clicking hides focus + if focus_visible.0 { + focus_visible.0 = false; + } + // Search for a focusable parent entity, defaulting to window if none. + if let Ok(window) = windows.single() { + commands + .entity(ev.target()) + .trigger(AcquireFocus { window }); + } + } +} + /// Observer function which handles tab navigation. /// /// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events, @@ -337,7 +389,7 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut trigger: On>, nav: TabNavigation, mut focus: ResMut, mut visible: ResMut, @@ -365,7 +417,7 @@ pub fn handle_tab_navigation( visible.0 = true; } Err(e) => { - warn!("Tab navigation error: {}", e); + warn!("Tab navigation error: {e}"); // This failure mode is recoverable, but still indicates a problem. if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e { trigger.propagate(false); diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9f78fc009d..1edc0e317e 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_internal" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "An internal Bevy crate used to facilitate optional dynamic linking via the 'dynamic_linking' feature" homepage = "https://bevy.org" @@ -28,6 +28,12 @@ detailed_trace = ["bevy_ecs/detailed_trace", "bevy_render?/detailed_trace"] sysinfo_plugin = ["bevy_diagnostic/sysinfo_plugin"] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = [ + "bevy_image/compressed_image_saver", + "bevy_render/compressed_image_saver", +] + # Texture formats that have specific rendering support (HDR enabled by default) basis-universal = ["bevy_image/basis-universal", "bevy_render/basis-universal"] exr = ["bevy_image/exr", "bevy_render/exr"] @@ -100,6 +106,7 @@ serialize = [ "bevy_window?/serialize", "bevy_winit?/serialize", "bevy_platform/serialize", + "bevy_render/serialize", ] multi_threaded = [ "std", @@ -346,80 +353,84 @@ web = [ hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] +debug = ["bevy_utils/debug"] + [dependencies] # bevy (no_std) -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev", default-features = false } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false, features = [ +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev", default-features = false } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false, features = [ +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", "nostd-libm", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "smallvec", ] } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev", default-features = false, features = [ +bevy_time = { path = "../bevy_time", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev", default-features = false, features = [ +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev", default-features = false, features = [ "bevy-support", "bevy_reflect", ] } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } # bevy (std required) -bevy_log = { path = "../bevy_log", version = "0.16.0-dev", optional = true } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev", optional = true } # bevy (optional) -bevy_a11y = { path = "../bevy_a11y", optional = true, version = "0.16.0-dev", features = [ +bevy_a11y = { path = "../bevy_a11y", optional = true, version = "0.17.0-dev", features = [ "bevy_reflect", ] } -bevy_animation = { path = "../bevy_animation", optional = true, version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", optional = true, version = "0.16.0-dev" } -bevy_audio = { path = "../bevy_audio", optional = true, version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_animation = { path = "../bevy_animation", optional = true, version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", optional = true, version = "0.17.0-dev" } +bevy_audio = { path = "../bevy_audio", optional = true, version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", optional = true, version = "0.17.0-dev", default-features = false, features = [ "alloc", "bevy_reflect", ] } -bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" } -bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" } -bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" } -bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" } -bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.16.0-dev", default-features = false } -bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", optional = true, version = "0.16.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.17.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.17.0-dev" } +bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.17.0-dev" } +bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.17.0-dev" } +bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false } +bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", optional = true, version = "0.16.0-dev" } -bevy_remote = { path = "../bevy_remote", optional = true, version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", optional = true, version = "0.16.0-dev" } -bevy_scene = { path = "../bevy_scene", optional = true, version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.16.0-dev" } -bevy_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", optional = true, version = "0.17.0-dev" } +bevy_remote = { path = "../bevy_remote", optional = true, version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", optional = true, version = "0.17.0-dev" } +bevy_scene = { path = "../bevy_scene", optional = true, version = "0.17.0-dev" } +bevy_solari = { path = "../bevy_solari", optional = true, version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.17.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_app", "bevy_reflect", ] } -bevy_text = { path = "../bevy_text", optional = true, version = "0.16.0-dev" } -bevy_ui = { path = "../bevy_ui", optional = true, version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_text = { path = "../bevy_text", optional = true, version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", optional = true, version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_winit = { path = "../bevy_winit", optional = true, version = "0.16.0-dev", default-features = false } +bevy_winit = { path = "../bevy_winit", optional = true, version = "0.17.0-dev", default-features = false } [lints] workspace = true diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 274364882e..b9934088f1 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -29,6 +29,8 @@ pub use bevy_audio as audio; pub use bevy_color as color; #[cfg(feature = "bevy_core_pipeline")] pub use bevy_core_pipeline as core_pipeline; +#[cfg(feature = "bevy_core_widgets")] +pub use bevy_core_widgets as core_widgets; #[cfg(feature = "bevy_dev_tools")] pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; @@ -60,6 +62,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_solari")] +pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; #[cfg(feature = "bevy_state")] diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index e6d8899d63..3dcfa27794 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_log" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides logging for Bevy Engine" homepage = "https://bevy.org" @@ -14,10 +14,10 @@ trace_tracy_memory = ["dep:tracy-client"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } # other tracing-subscriber = { version = "0.3.1", features = [ @@ -40,12 +40,12 @@ android_log-sys = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] tracing-wasm = "0.2.1" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } [target.'cfg(target_os = "ios")'.dependencies] -tracing-oslog = "0.2" +tracing-oslog = "0.3" [lints] workspace = true diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index 7a80a21cc3..9a614d2b89 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -314,7 +314,7 @@ impl Plugin for LogPlugin { .and_then(|source| source.downcast_ref::()) .map(|parse_err| { // we cannot use the `error!` macro here because the logger is not ready yet. - eprintln!("LogPlugin failed to parse filter from env: {}", parse_err); + eprintln!("LogPlugin failed to parse filter from env: {parse_err}"); }); Ok::(EnvFilter::builder().parse_lossy(&default_filter)) diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index bc5989f0f3..b998ae4fd4 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_macro_utils" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A collection of utils for Bevy Engine" homepage = "https://bevy.org" diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index c420c3fe3c..3fad8e6209 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_math" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides math functionality for Bevy Engine" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ rust-version = "1.85.0" [dependencies] glam = { version = "0.29.3", default-features = false, features = ["bytemuck"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = [ +derive_more = { version = "2", default-features = false, features = [ "from", "into", ] } @@ -25,7 +25,7 @@ approx = { version = "0.5", default-features = false, optional = true } rand = { version = "0.8", default-features = false, optional = true } rand_distr = { version = "0.4.3", optional = true } smallvec = { version = "1.11" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", ], optional = true } variadics_please = "1.1" diff --git a/crates/bevy_math/src/cubic_splines/mod.rs b/crates/bevy_math/src/cubic_splines/mod.rs index 1b04603a73..3ea99a60b0 100644 --- a/crates/bevy_math/src/cubic_splines/mod.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -1347,6 +1347,7 @@ pub struct RationalSegment { /// The width of the domain of this segment. pub knot_span: f32, } + impl> RationalSegment

{ /// Instantaneous position of a point at parametric value `t` in `[0, 1]`. #[inline] diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 94e7b0151e..c1c45e655b 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1096,6 +1096,10 @@ mod tests { }); } + #[expect( + clippy::neg_multiply, + reason = "Clippy doesn't like this, but it's correct" + )] #[test] fn mapping() { let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index d666849840..9cb379706c 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -35,6 +35,7 @@ pub struct Circle { /// The radius of the circle pub radius: f32, } + impl Primitive2d for Circle {} impl Default for Circle { @@ -124,6 +125,7 @@ pub struct Arc2d { /// Half the angle defining the arc pub half_angle: f32, } + impl Primitive2d for Arc2d {} impl Default for Arc2d { @@ -290,6 +292,7 @@ pub struct CircularSector { #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } + impl Primitive2d for CircularSector {} impl Default for CircularSector { @@ -433,6 +436,7 @@ pub struct CircularSegment { #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } + impl Primitive2d for CircularSegment {} impl Default for CircularSegment { @@ -453,6 +457,7 @@ impl Measured2d for CircularSegment { self.chord_length() + self.arc_length() } } + impl CircularSegment { /// Create a new [`CircularSegment`] from a `radius`, and an `angle` #[inline(always)] @@ -788,6 +793,7 @@ pub struct Ellipse { /// This corresponds to the two perpendicular radii defining the ellipse. pub half_size: Vec2, } + impl Primitive2d for Ellipse {} impl Default for Ellipse { @@ -939,6 +945,7 @@ pub struct Annulus { /// The outer circle of the annulus pub outer_circle: Circle, } + impl Primitive2d for Annulus {} impl Default for Annulus { @@ -1036,6 +1043,7 @@ pub struct Rhombus { /// Size of the horizontal and vertical diagonals of the rhombus pub half_diagonals: Vec2, } + impl Primitive2d for Rhombus {} impl Default for Rhombus { @@ -1171,6 +1179,7 @@ pub struct Plane2d { /// The normal of the plane. The plane will be placed perpendicular to this direction pub normal: Dir2, } + impl Primitive2d for Plane2d {} impl Default for Plane2d { @@ -1213,6 +1222,7 @@ pub struct Line2d { /// and its opposite direction pub direction: Dir2, } + impl Primitive2d for Line2d {} /// A line segment defined by two endpoints in 2D space. @@ -1232,6 +1242,7 @@ pub struct Segment2d { /// The endpoints of the line segment. pub vertices: [Vec2; 2], } + impl Primitive2d for Segment2d {} impl Segment2d { @@ -1504,6 +1515,7 @@ pub struct Polyline2d { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec2; N], } + impl Primitive2d for Polyline2d {} impl FromIterator for Polyline2d { @@ -1573,6 +1585,7 @@ pub struct Triangle2d { /// The vertices of the triangle pub vertices: [Vec2; 3], } + impl Primitive2d for Triangle2d {} impl Default for Triangle2d { @@ -1745,6 +1758,7 @@ pub struct Rectangle { /// Half of the width and height of the rectangle pub half_size: Vec2, } + impl Primitive2d for Rectangle {} impl Default for Rectangle { @@ -1838,6 +1852,7 @@ pub struct Polygon { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec2; N], } + impl Primitive2d for Polygon {} impl FromIterator for Polygon { @@ -1892,6 +1907,7 @@ pub struct ConvexPolygon { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] vertices: [Vec2; N], } + impl Primitive2d for ConvexPolygon {} /// An error that happens when creating a [`ConvexPolygon`]. @@ -2013,6 +2029,7 @@ pub struct RegularPolygon { /// The number of sides pub sides: u32, } + impl Primitive2d for RegularPolygon {} impl Default for RegularPolygon { @@ -2160,6 +2177,7 @@ pub struct Capsule2d { /// Half the height of the capsule, excluding the semicircles pub half_length: f32, } + impl Primitive2d for Capsule2d {} impl Default for Capsule2d { diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index ea5ccd6e2d..86aa6c5bdf 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -31,6 +31,7 @@ pub struct Sphere { /// The radius of the sphere pub radius: f32, } + impl Primitive3d for Sphere {} impl Default for Sphere { @@ -105,6 +106,7 @@ pub struct Plane3d { /// Half of the width and height of the plane pub half_size: Vec2, } + impl Primitive3d for Plane3d {} impl Default for Plane3d { @@ -175,6 +177,7 @@ pub struct InfinitePlane3d { /// The normal of the plane. The plane will be placed perpendicular to this direction pub normal: Dir3, } + impl Primitive3d for InfinitePlane3d {} impl Default for InfinitePlane3d { @@ -351,6 +354,7 @@ pub struct Line3d { /// The direction of the line pub direction: Dir3, } + impl Primitive3d for Line3d {} /// A line segment defined by two endpoints in 3D space. @@ -370,6 +374,7 @@ pub struct Segment3d { /// The endpoints of the line segment. pub vertices: [Vec3; 2], } + impl Primitive3d for Segment3d {} impl Segment3d { @@ -578,6 +583,7 @@ pub struct Polyline3d { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec3; N], } + impl Primitive3d for Polyline3d {} impl FromIterator for Polyline3d { @@ -648,6 +654,7 @@ pub struct Cuboid { /// Half of the width, height and depth of the cuboid pub half_size: Vec3, } + impl Primitive3d for Cuboid {} impl Default for Cuboid { @@ -742,6 +749,7 @@ pub struct Cylinder { /// The half height of the cylinder pub half_height: f32, } + impl Primitive3d for Cylinder {} impl Default for Cylinder { @@ -820,6 +828,7 @@ pub struct Capsule3d { /// Half the height of the capsule, excluding the hemispheres pub half_length: f32, } + impl Primitive3d for Capsule3d {} impl Default for Capsule3d { @@ -890,6 +899,7 @@ pub struct Cone { /// The height of the cone pub height: f32, } + impl Primitive3d for Cone {} impl Default for Cone { @@ -974,6 +984,7 @@ pub struct ConicalFrustum { /// The height of the frustum pub height: f32, } + impl Primitive3d for ConicalFrustum {} impl Default for ConicalFrustum { @@ -1030,6 +1041,7 @@ pub struct Torus { #[doc(alias = "radius_of_revolution")] pub major_radius: f32, } + impl Primitive3d for Torus {} impl Default for Torus { @@ -1326,6 +1338,7 @@ pub struct Tetrahedron { /// The vertices of the tetrahedron. pub vertices: [Vec3; 4], } + impl Primitive3d for Tetrahedron {} impl Default for Tetrahedron { @@ -1433,6 +1446,7 @@ pub struct Extrusion { /// Half of the depth of the extrusion pub half_depth: f32, } + impl Primitive3d for Extrusion {} impl Extrusion { diff --git a/crates/bevy_math/src/primitives/polygon.rs b/crates/bevy_math/src/primitives/polygon.rs index 20d35b552c..9aa261b297 100644 --- a/crates/bevy_math/src/primitives/polygon.rs +++ b/crates/bevy_math/src/primitives/polygon.rs @@ -34,6 +34,7 @@ struct SweepLineEvent { /// Type of the vertex (left or right) endpoint: Endpoint, } + impl SweepLineEvent { #[cfg_attr( not(feature = "alloc"), @@ -46,17 +47,21 @@ impl SweepLineEvent { } } } + impl PartialEq for SweepLineEvent { fn eq(&self, other: &Self) -> bool { self.position() == other.position() } } + impl Eq for SweepLineEvent {} + impl PartialOrd for SweepLineEvent { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } + impl Ord for SweepLineEvent { fn cmp(&self, other: &Self) -> Ordering { xy_order(self.position(), other.position()) @@ -129,11 +134,13 @@ struct Segment { left: Vec2, right: Vec2, } + impl PartialEq for Segment { fn eq(&self, other: &Self) -> bool { self.edge_index == other.edge_index } } + impl Eq for Segment {} impl PartialOrd for Segment { @@ -141,6 +148,7 @@ impl PartialOrd for Segment { Some(self.cmp(other)) } } + impl Ord for Segment { fn cmp(&self, other: &Self) -> Ordering { self.left diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index a235fea5ef..7807acbb9d 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mesh" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides mesh types for Bevy Engine" homepage = "https://bevy.org" @@ -10,16 +10,16 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -27,12 +27,22 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea # other bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5" } -wgpu-types = { version = "24", default-features = false } -serde = { version = "1", features = ["derive"] } +wgpu-types = { version = "25", default-features = false } +serde = { version = "1", default-features = false, features = [ + "derive", +], optional = true } hexasphere = "15.0" thiserror = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } +[dev-dependencies] +serde_json = "1.0.140" + +[features] +default = [] +## Adds serialization support through `serde`. +serialize = ["dep:serde", "wgpu-types/serde"] + [lints] workspace = true diff --git a/crates/bevy_mesh/src/index.rs b/crates/bevy_mesh/src/index.rs index d2497e2c50..87d81cc3f2 100644 --- a/crates/bevy_mesh/src/index.rs +++ b/crates/bevy_mesh/src/index.rs @@ -1,6 +1,8 @@ use bevy_reflect::Reflect; use core::iter; use core::iter::FusedIterator; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::IndexFormat; @@ -69,8 +71,9 @@ pub enum MeshTrianglesError { /// An array of indices into the [`VertexAttributeValues`](super::VertexAttributeValues) for a mesh. /// /// It describes the order in which the vertex attributes should be joined into faces. -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, PartialEq)] #[reflect(Clone)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub enum Indices { U16(Vec), U32(Vec), @@ -163,6 +166,7 @@ impl Iterator for IndicesIter<'_> { } impl<'a> ExactSizeIterator for IndicesIter<'a> {} + impl<'a> FusedIterator for IndicesIter<'a> {} impl From<&Indices> for IndexFormat { diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index e4868dbf69..3492788c4b 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -7,12 +7,18 @@ use super::{ MeshVertexAttributeId, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, }; +#[cfg(feature = "serialize")] +use crate::SerializedMeshAttributeData; use alloc::collections::BTreeMap; use bevy_asset::{Asset, Handle, RenderAssetUsages}; use bevy_image::Image; use bevy_math::{primitives::Triangle3d, *}; +#[cfg(feature = "serialize")] +use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; use bytemuck::cast_slice; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::warn; use wgpu_types::{VertexAttribute, VertexFormat, VertexStepMode}; @@ -104,7 +110,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// - Vertex winding order: by default, `StandardMaterial.cull_mode` is `Some(Face::Back)`, /// which means that Bevy would *only* render the "front" of each triangle, which /// is the side of the triangle from where the vertices appear in a *counter-clockwise* order. -#[derive(Asset, Debug, Clone, Reflect)] +#[derive(Asset, Debug, Clone, Reflect, PartialEq)] #[reflect(Clone)] pub struct Mesh { #[reflect(ignore, clone)] @@ -119,6 +125,21 @@ pub struct Mesh { morph_targets: Option>, morph_target_names: Option>, pub asset_usage: RenderAssetUsages, + /// Whether or not to build a BLAS for use with `bevy_solari` raytracing. + /// + /// Note that this is _not_ whether the mesh is _compatible_ with `bevy_solari` raytracing. + /// This field just controls whether or not a BLAS gets built for this mesh, assuming that + /// the mesh is compatible. + /// + /// The use case for this field is using lower-resolution proxy meshes for raytracing (to save on BLAS memory usage), + /// while using higher-resolution meshes for raster. You can set this field to true for the lower-resolution proxy mesh, + /// and to false for the high-resolution raster mesh. + /// + /// Alternatively, you can use the same mesh for both raster and raytracing, with this field set to true. + /// + /// Does nothing if not used with `bevy_solari`, or if the mesh is not compatible + /// with `bevy_solari` (see `bevy_solari`'s docs). + pub enable_raytracing: bool, } impl Mesh { @@ -192,6 +213,10 @@ impl Mesh { pub const ATTRIBUTE_JOINT_INDEX: MeshVertexAttribute = MeshVertexAttribute::new("Vertex_JointIndex", 7, VertexFormat::Uint16x4); + /// The first index that can be used for custom vertex attributes. + /// Only the attributes with an index below this are used by Bevy. + pub const FIRST_AVAILABLE_CUSTOM_ATTRIBUTE: u64 = 8; + /// Construct a new mesh. You need to provide a [`PrimitiveTopology`] so that the /// renderer knows how to treat the vertex data. Most of the time this will be /// [`PrimitiveTopology::TriangleList`]. @@ -203,6 +228,7 @@ impl Mesh { morph_targets: None, morph_target_names: None, asset_usage, + enable_raytracing: true, } } @@ -1236,6 +1262,133 @@ impl core::ops::Mul for Transform { } } +/// A version of [`Mesh`] suitable for serializing for short-term transfer. +/// +/// [`Mesh`] does not implement [`Serialize`] / [`Deserialize`] because it is made with the renderer in mind. +/// It is not a general-purpose mesh implementation, and its internals are subject to frequent change. +/// As such, storing a [`Mesh`] on disk is highly discouraged. +/// +/// But there are still some valid use cases for serializing a [`Mesh`], namely transferring meshes between processes. +/// To support this, you can create a [`SerializedMesh`] from a [`Mesh`] with [`SerializedMesh::from_mesh`], +/// and then deserialize it with [`SerializedMesh::deserialize`]. The caveats are: +/// - The mesh representation is not valid across different versions of Bevy. +/// - This conversion is lossy. Only the following information is preserved: +/// - Primitive topology +/// - Vertex attributes +/// - Indices +/// - Custom attributes that were not specified with [`MeshDeserializer::add_custom_vertex_attribute`] will be ignored while deserializing. +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializedMesh { + primitive_topology: PrimitiveTopology, + attributes: Vec<(MeshVertexAttributeId, SerializedMeshAttributeData)>, + indices: Option, +} + +#[cfg(feature = "serialize")] +impl SerializedMesh { + /// Create a [`SerializedMesh`] from a [`Mesh`]. See the documentation for [`SerializedMesh`] for caveats. + pub fn from_mesh(mesh: Mesh) -> Self { + Self { + primitive_topology: mesh.primitive_topology, + attributes: mesh + .attributes + .into_iter() + .map(|(id, data)| { + ( + id, + SerializedMeshAttributeData::from_mesh_attribute_data(data), + ) + }) + .collect(), + indices: mesh.indices, + } + } + + /// Create a [`Mesh`] from a [`SerializedMesh`]. See the documentation for [`SerializedMesh`] for caveats. + /// + /// Use [`MeshDeserializer`] if you need to pass extra options to the deserialization process, such as specifying custom vertex attributes. + pub fn into_mesh(self) -> Mesh { + MeshDeserializer::default().deserialize(self) + } +} + +/// Use to specify extra options when deserializing a [`SerializedMesh`] into a [`Mesh`]. +#[cfg(feature = "serialize")] +pub struct MeshDeserializer { + custom_vertex_attributes: HashMap, MeshVertexAttribute>, +} + +#[cfg(feature = "serialize")] +impl Default for MeshDeserializer { + fn default() -> Self { + // Written like this so that the compiler can validate that we use all the built-in attributes. + // If you just added a new attribute and got a compile error, please add it to this list :) + const BUILTINS: [MeshVertexAttribute; Mesh::FIRST_AVAILABLE_CUSTOM_ATTRIBUTE as usize] = [ + Mesh::ATTRIBUTE_POSITION, + Mesh::ATTRIBUTE_NORMAL, + Mesh::ATTRIBUTE_UV_0, + Mesh::ATTRIBUTE_UV_1, + Mesh::ATTRIBUTE_TANGENT, + Mesh::ATTRIBUTE_COLOR, + Mesh::ATTRIBUTE_JOINT_WEIGHT, + Mesh::ATTRIBUTE_JOINT_INDEX, + ]; + Self { + custom_vertex_attributes: BUILTINS + .into_iter() + .map(|attribute| (attribute.name.into(), attribute)) + .collect(), + } + } +} + +#[cfg(feature = "serialize")] +impl MeshDeserializer { + /// Create a new [`MeshDeserializer`]. + pub fn new() -> Self { + Self::default() + } + + /// Register a custom vertex attribute to the deserializer. Custom vertex attributes that were not added with this method will be ignored while deserializing. + pub fn add_custom_vertex_attribute( + &mut self, + name: &str, + attribute: MeshVertexAttribute, + ) -> &mut Self { + self.custom_vertex_attributes.insert(name.into(), attribute); + self + } + + /// Deserialize a [`SerializedMesh`] into a [`Mesh`]. + /// + /// See the documentation for [`SerializedMesh`] for caveats. + pub fn deserialize(&self, serialized_mesh: SerializedMesh) -> Mesh { + Mesh { + attributes: + serialized_mesh + .attributes + .into_iter() + .filter_map(|(id, data)| { + let attribute = data.attribute.clone(); + let Some(data) = + data.try_into_mesh_attribute_data(&self.custom_vertex_attributes) + else { + warn!( + "Deserialized mesh contains custom vertex attribute {attribute:?} that \ + was not specified with `MeshDeserializer::add_custom_vertex_attribute`. Ignoring." + ); + return None; + }; + Some((id, data)) + }) + .collect(), + indices: serialized_mesh.indices, + ..Mesh::new(serialized_mesh.primitive_topology, RenderAssetUsages::default()) + } + } +} + /// Error that can occur when calling [`Mesh::merge`]. #[derive(Error, Debug, Clone)] #[error("Incompatible vertex attribute types {} and {}", self_attribute.name, other_attribute.map(|a| a.name).unwrap_or("None"))] @@ -1247,6 +1400,8 @@ pub struct MergeMeshError { #[cfg(test)] mod tests { use super::Mesh; + #[cfg(feature = "serialize")] + use super::SerializedMesh; use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; use crate::PrimitiveTopology; use bevy_asset::RenderAssetUsages; @@ -1551,4 +1706,26 @@ mod tests { mesh.triangles().unwrap().collect::>() ); } + + #[cfg(feature = "serialize")] + #[test] + fn serialize_deserialize_mesh() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + + mesh.insert_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![[0., 0., 0.], [2., 0., 0.], [0., 1., 0.], [0., 0., 1.]], + ); + mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3])); + + let serialized_mesh = SerializedMesh::from_mesh(mesh.clone()); + let serialized_string = serde_json::to_string(&serialized_mesh).unwrap(); + let serialized_mesh_from_string: SerializedMesh = + serde_json::from_str(&serialized_string).unwrap(); + let deserialized_mesh = serialized_mesh_from_string.into_mesh(); + assert_eq!(mesh, deserialized_mesh); + } } diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index a8ff3be037..fdeeeacc31 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -117,6 +117,7 @@ pub struct MorphWeights { /// The first mesh primitive assigned to these weights first_mesh: Option>, } + impl MorphWeights { pub fn new( weights: Vec, @@ -160,6 +161,7 @@ impl MorphWeights { pub struct MeshMorphWeights { weights: Vec, } + impl MeshMorphWeights { pub fn new(weights: Vec) -> Result { if weights.len() > MAX_MORPH_WEIGHTS { @@ -198,6 +200,7 @@ pub struct MorphAttributes { /// animated, as the `w` component is the sign and cannot be animated. pub tangent: Vec3, } + impl From<[Vec3; 3]> for MorphAttributes { fn from([position, normal, tangent]: [Vec3; 3]) -> Self { MorphAttributes { @@ -207,6 +210,7 @@ impl From<[Vec3; 3]> for MorphAttributes { } } } + impl MorphAttributes { /// How many components `MorphAttributes` has. /// diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 949e355b4c..fd683ef60d 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -2,13 +2,17 @@ use alloc::sync::Arc; use bevy_derive::EnumVariantMeta; use bevy_ecs::resource::Resource; use bevy_math::Vec3; +#[cfg(feature = "serialize")] +use bevy_platform::collections::HashMap; use bevy_platform::collections::HashSet; use bytemuck::cast_slice; use core::hash::{Hash, Hasher}; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::{BufferAddress, VertexAttribute, VertexFormat, VertexStepMode}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct MeshVertexAttribute { /// The friendly name of the vertex attribute pub name: &'static str, @@ -22,6 +26,37 @@ pub struct MeshVertexAttribute { pub format: VertexFormat, } +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct SerializedMeshVertexAttribute { + pub(crate) name: String, + pub(crate) id: MeshVertexAttributeId, + pub(crate) format: VertexFormat, +} + +#[cfg(feature = "serialize")] +impl SerializedMeshVertexAttribute { + pub(crate) fn from_mesh_vertex_attribute(attribute: MeshVertexAttribute) -> Self { + Self { + name: attribute.name.to_string(), + id: attribute.id, + format: attribute.format, + } + } + + pub(crate) fn try_into_mesh_vertex_attribute( + self, + possible_attributes: &HashMap, MeshVertexAttribute>, + ) -> Option { + let attr = possible_attributes.get(self.name.as_str())?; + if attr.id == self.id { + Some(*attr) + } else { + None + } + } +} + impl MeshVertexAttribute { pub const fn new(name: &'static str, id: u64, format: VertexFormat) -> Self { Self { @@ -37,6 +72,7 @@ impl MeshVertexAttribute { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct MeshVertexAttributeId(u64); impl From for MeshVertexAttributeId { @@ -132,12 +168,42 @@ impl VertexAttributeDescriptor { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct MeshAttributeData { pub(crate) attribute: MeshVertexAttribute, pub(crate) values: VertexAttributeValues, } +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct SerializedMeshAttributeData { + pub(crate) attribute: SerializedMeshVertexAttribute, + pub(crate) values: VertexAttributeValues, +} + +#[cfg(feature = "serialize")] +impl SerializedMeshAttributeData { + pub(crate) fn from_mesh_attribute_data(data: MeshAttributeData) -> Self { + Self { + attribute: SerializedMeshVertexAttribute::from_mesh_vertex_attribute(data.attribute), + values: data.values, + } + } + + pub(crate) fn try_into_mesh_attribute_data( + self, + possible_attributes: &HashMap, MeshVertexAttribute>, + ) -> Option { + let attribute = self + .attribute + .try_into_mesh_vertex_attribute(possible_attributes)?; + Some(MeshAttributeData { + attribute, + values: self.values, + }) + } +} + /// Compute a vector whose direction is the normal of the triangle formed by /// points a, b, c, and whose magnitude is double the area of the triangle. This /// is useful for computing smooth normals where the contributing normals are @@ -167,7 +233,8 @@ pub fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { /// Contains an array where each entry describes a property of a single vertex. /// Matches the [`VertexFormats`](VertexFormat). -#[derive(Clone, Debug, EnumVariantMeta)] +#[derive(Clone, Debug, EnumVariantMeta, PartialEq)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub enum VertexAttributeValues { Float32(Vec), Sint32(Vec), diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index 7428504adc..82c2d86553 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mikktspace" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" authors = [ "Benjamin Wasty ", diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index f74e05098b..12efbf5d62 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -7,9 +7,7 @@ unsafe_op_in_unsafe_fn, clippy::all, clippy::undocumented_unsafe_blocks, - clippy::ptr_cast_constness, - // FIXME(15321): solve CI failures, then replace with `#![expect()]`. - missing_docs + clippy::ptr_cast_constness )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( @@ -18,6 +16,10 @@ )] #![no_std] +//! An implementation of [Mikkelsen's algorithm] for tangent space generation. +//! +//! [Mikkelsen's algorithm]: http://www.mikktspace.com + #[cfg(feature = "std")] extern crate std; diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 41a058b522..f2e973eae5 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_pbr" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Adds PBR rendering to Bevy Engine" homepage = "https://bevy.org" @@ -31,22 +31,22 @@ meshlet_processor = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -54,7 +54,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea bitflags = "2.3" fixedbitset = "0.5" thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } # meshlet lz4_flex = { version = "0.11", default-features = false, features = [ "frame", diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index 8b08751428..a55403630a 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -309,7 +309,7 @@ impl ExtractComponent for Atmosphere { type Out = Atmosphere; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } @@ -405,7 +405,7 @@ impl ExtractComponent for AtmosphereSettings { type Out = AtmosphereSettings; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index 851447d760..e09b27c590 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -181,7 +181,7 @@ impl ViewNode for RenderSkyNode { view_uniforms_offset, lights_uniforms_offset, render_sky_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl index e488656df4..f8298272ca 100644 --- a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl +++ b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl @@ -1,3 +1,5 @@ +enable dual_source_blending; + #import bevy_pbr::atmosphere::{ types::{Atmosphere, AtmosphereSettings}, bindings::{atmosphere, view, atmosphere_transforms}, @@ -19,9 +21,11 @@ #endif struct RenderSkyOutput { - @location(0) inscattering: vec4, #ifdef DUAL_SOURCE_BLENDING - @location(0) @second_blend_source transmittance: vec4, + @location(0) @blend_src(0) inscattering: vec4, + @location(0) @blend_src(1) transmittance: vec4, +#else + @location(0) inscattering: vec4, #endif } diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index 8c57fb8eb9..aa682fbb03 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -1,7 +1,5 @@ use bevy_asset::{load_embedded_asset, Handle}; -use bevy_core_pipeline::{ - core_3d::Camera3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state, -}; +use bevy_core_pipeline::{core_3d::Camera3d, FullscreenShader}; use bevy_ecs::{ component::Component, entity::Entity, @@ -36,7 +34,8 @@ pub(crate) struct AtmosphereBindGroupLayouts { pub(crate) struct RenderSkyBindGroupLayouts { pub render_sky: BindGroupLayout, pub render_sky_msaa: BindGroupLayout, - pub shader: Handle, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, } impl FromWorld for AtmosphereBindGroupLayouts { @@ -205,7 +204,8 @@ impl FromWorld for RenderSkyBindGroupLayouts { Self { render_sky, render_sky_msaa, - shader: load_embedded_asset!(world, "render_sky.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "render_sky.wgsl"), } } } @@ -358,7 +358,7 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { self.render_sky_msaa.clone() }], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState { @@ -368,7 +368,7 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { }, zero_initialize_workgroup_memory: false, fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "main".into(), targets: vec![Some(ColorTargetState { @@ -560,7 +560,7 @@ pub(super) fn prepare_atmosphere_transforms( }; for (entity, view) in &views { - let world_from_view = view.world_from_view.compute_matrix(); + let world_from_view = view.world_from_view.to_matrix(); let camera_z = world_from_view.z_axis.truncate(); let camera_y = world_from_view.y_axis.truncate(); let atmo_z = camera_z diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 1b7b3563d7..b0c0fb6347 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -353,7 +353,7 @@ pub(crate) fn assign_objects_to_clusters( let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size); - let world_from_view = camera_transform.compute_matrix(); + let world_from_view = camera_transform.to_matrix(); let view_from_world_scale = camera_transform.compute_transform().scale.recip(); let view_from_world_scale_max = view_from_world_scale.abs().max_element(); let view_from_world = world_from_view.inverse(); diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 3113333be3..3559cb52d5 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -535,12 +535,12 @@ pub fn extract_clusters( continue; } - let num_entities: usize = clusters + let entity_count: usize = clusters .clusterable_objects .iter() .map(|l| l.entities.len()) .sum(); - let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + num_entities); + let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + entity_count); for cluster_objects in &clusters.clusterable_objects { data.push(ExtractedClusterableObjectElement::ClusterHeader( cluster_objects.counts, diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index dd77d2088d..5618b31831 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -69,7 +69,7 @@ pub struct ClusteredDecalPlugin; /// An object that projects a decal onto surfaces within its bounds. /// /// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It -/// projects the given [`Self::image`] onto surfaces in the +Z direction (thus +/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus /// you may find [`Transform::looking_at`] useful). /// /// Clustered decals are the highest-quality types of decals that Bevy supports, diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index 7d82946346..757ecff2c3 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -132,7 +132,7 @@ impl MaterialExtension for ForwardDecalMaterialExt { } if let Some(label) = &mut descriptor.label { - *label = format!("forward_decal_{}", label).into(); + *label = format!("forward_decal_{label}").into(); } Ok(()) diff --git a/crates/bevy_pbr/src/decal/forward_decal.wgsl b/crates/bevy_pbr/src/decal/forward_decal.wgsl index ce24d57bf5..f0414bc807 100644 --- a/crates/bevy_pbr/src/decal/forward_decal.wgsl +++ b/crates/bevy_pbr/src/decal/forward_decal.wgsl @@ -10,7 +10,7 @@ } #import bevy_render::maths::project_onto -@group(2) @binding(200) +@group(3) @binding(200) var inv_depth_fade_factor: f32; struct ForwardDecalInformation { diff --git a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl index 843ed2bbf6..7c14eea4ba 100644 --- a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl +++ b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl @@ -29,7 +29,7 @@ struct PbrDeferredLightingDepthId { _webgl2_padding_2: f32, #endif } -@group(1) @binding(0) +@group(2) @binding(0) var depth_id: PbrDeferredLightingDepthId; @vertex diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 65be474e65..b8f3a660c0 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -184,9 +184,9 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { return Ok(()); }; - let bind_group_1 = render_context.render_device().create_bind_group( - "deferred_lighting_layout_group_1", - &deferred_lighting_layout.bind_group_layout_1, + let bind_group_2 = render_context.render_device().create_bind_group( + "deferred_lighting_layout_group_2", + &deferred_lighting_layout.bind_group_layout_2, &BindGroupEntries::single(deferred_lighting_pass_id_binding), ); @@ -208,7 +208,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { render_pass.set_render_pipeline(pipeline); render_pass.set_bind_group( 0, - &mesh_view_bind_group.value, + &mesh_view_bind_group.main, &[ view_uniform_offset.offset, view_lights_offset.offset, @@ -218,7 +218,8 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { **view_environment_map_offset, ], ); - render_pass.set_bind_group(1, &bind_group_1, &[]); + render_pass.set_bind_group(1, &mesh_view_bind_group.binding_array, &[]); + render_pass.set_bind_group(2, &bind_group_2, &[]); render_pass.draw(0..3, 0..1); Ok(()) @@ -228,7 +229,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { #[derive(Resource)] pub struct DeferredLightingLayout { mesh_pipeline: MeshPipeline, - bind_group_layout_1: BindGroupLayout, + bind_group_layout_2: BindGroupLayout, deferred_lighting_shader: Handle, } @@ -346,11 +347,13 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into()); + let layout = self.mesh_pipeline.get_view_layout(key.into()); RenderPipelineDescriptor { label: Some("deferred_lighting_pipeline".into()), layout: vec![ - self.mesh_pipeline.get_view_layout(key.into()).clone(), - self.bind_group_layout_1.clone(), + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + self.bind_group_layout_2.clone(), ], vertex: VertexState { shader: self.deferred_lighting_shader.clone(), @@ -408,7 +411,7 @@ impl FromWorld for DeferredLightingLayout { ); Self { mesh_pipeline: world.resource::().clone(), - bind_group_layout_1: layout, + bind_group_layout_2: layout, deferred_lighting_shader: load_embedded_asset!(world, "deferred_lighting.wgsl"), } } @@ -449,6 +452,7 @@ pub fn prepare_deferred_lighting_pipelines( ), Has>, Has>, + Has, )>, ) { for ( @@ -461,12 +465,13 @@ pub fn prepare_deferred_lighting_pipelines( (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), has_environment_maps, has_irradiance_volumes, + skip_deferred_lighting, ) in &views { - // If there is no deferred prepass, remove the old pipeline if there was - // one. This handles the case in which a view using deferred stops using - // it. - if !deferred_prepass { + // If there is no deferred prepass or we want to skip the deferred lighting pass, + // remove the old pipeline if there was one. This handles the case in which a + // view using deferred stops using it. + if !deferred_prepass || skip_deferred_lighting { commands.entity(entity).remove::(); continue; } @@ -552,3 +557,14 @@ pub fn prepare_deferred_lighting_pipelines( .insert(DeferredLightingPipeline { pipeline_id }); } } + +/// Component to skip running the deferred lighting pass in [`DeferredOpaquePass3dPbrLightingNode`] for a specific view. +/// +/// This works like [`crate::PbrPlugin::add_default_deferred_lighting_plugin`], but is per-view instead of global. +/// +/// Useful for cases where you want to generate a gbuffer, but skip the built-in deferred lighting pass +/// to run your own custom lighting pass instead. +/// +/// Insert this component in the render world only. +#[derive(Component, Clone, Copy, Default)] +pub struct SkipDeferredLighting; diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 945bc9c55b..517ef0eb5c 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -152,8 +152,8 @@ fn shader_ref(path: PathBuf) -> ShaderRef { const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle = weak_handle!("69187376-3dea-4d0f-b3f5-185bde63d6a2"); -pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 26; -pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 27; +pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 18; +pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 19; /// Sets up the entire PBR infrastructure of bevy. pub struct PbrPlugin { diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs index db255722b3..cfbe99963b 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -48,6 +48,7 @@ impl Default for AmbientLight { } } } + impl AmbientLight { pub const NONE: AmbientLight = AmbientLight { color: Color::WHITE, diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 4ff4662bb2..e50ffcc0fc 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ use bevy_math::{ops, Mat4, Vec3A, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{ - camera::{Camera, CameraProjection, Projection}, + camera::{Camera, Projection}, extract_component::ExtractComponent, extract_resource::ExtractResource, mesh::Mesh3d, @@ -341,7 +341,7 @@ pub fn build_directional_light_cascades( .iter() .filter_map(|(entity, transform, projection, camera)| { if camera.is_active { - Some((entity, projection, transform.compute_matrix())) + Some((entity, projection, transform.to_matrix())) } else { None } @@ -357,7 +357,7 @@ pub fn build_directional_light_cascades( // light_to_world has orthogonal upper-left 3x3 and zero translation. // Even though only the direction (i.e. rotation) of the light matters, we don't constrain // users to not change any other aspects of the transform - there's no guarantee - // `transform.compute_matrix()` will give us a matrix with our desired properties. + // `transform.to_matrix()` will give us a matrix with our desired properties. // Instead, we directly create a good matrix from just the rotation. let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); let light_to_world_inverse = world_from_light.inverse(); @@ -628,7 +628,7 @@ pub fn update_point_light_frusta( for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { let world_from_view = view_translation * *view_rotation; - let clip_from_world = clip_from_view * world_from_view.compute_matrix().inverse(); + let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); *frustum = Frustum::from_clip_from_world_custom_far( &clip_from_world, diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index b31801f757..fa55b4d94a 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -192,7 +192,7 @@ impl ExtractInstance for EnvironmentMapIds { type QueryFilter = (); - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(EnvironmentMapIds { diffuse: item.diffuse_map.id(), specular: item.specular_map.id(), diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index 431d1245a2..e2dea463f2 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -17,11 +17,12 @@ //! documentation in the `bevy-baked-gi` project for more details on this //! workflow. //! -//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes that can -//! be arbitrarily scaled, rotated, and positioned in a scene with the -//! [`bevy_transform::components::Transform`] component. The 3D voxel grid will -//! be stretched to fill the interior of the cube, and the illumination from the -//! irradiance volume will apply to all fragments within that bounding region. +//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes, centered +//! on the origin, that can be arbitrarily scaled, rotated, and positioned in a +//! scene with the [`bevy_transform::components::Transform`] component. The 3D +//! voxel grid will be stretched to fill the interior of the cube, with linear +//! interpolation, and the illumination from the irradiance volume will apply to +//! all fragments within that bounding region. //! //! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in //! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of @@ -154,7 +155,7 @@ use crate::{ MAX_VIEW_LIGHT_PROBES, }; -use super::LightProbeComponent; +use super::{LightProbe, LightProbeComponent}; /// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can /// overflow the number of texture bindings when deferred rendering is in use @@ -164,8 +165,12 @@ pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "w /// The component that defines an irradiance volume. /// /// See [`crate::irradiance_volume`] for detailed information. +/// +/// This component requires the [`LightProbe`] component, and is typically used with +/// [`bevy_transform::components::Transform`] to place the volume appropriately. #[derive(Clone, Reflect, Component, Debug)] #[reflect(Component, Default, Debug, Clone)] +#[require(LightProbe)] pub struct IrradianceVolume { /// The 3D texture that represents the ambient cubes, encoded in the format /// described in [`crate::irradiance_volume`]. diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 74710ce1d5..bfce2f1e26 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -378,7 +378,7 @@ fn gather_environment_map_uniform( let environment_map_uniform = if let Some(environment_map_light) = environment_map_light { EnvironmentMapUniform { transform: Transform::from_rotation(environment_map_light.rotation) - .compute_matrix() + .to_matrix() .inverse(), } } else { @@ -595,7 +595,7 @@ where ) -> Option> { environment_map.id(image_assets).map(|id| LightProbeInfo { world_from_light: light_probe_transform.affine(), - light_from_world: light_probe_transform.compute_matrix().inverse(), + light_from_world: light_probe_transform.to_matrix().inverse(), asset_id: id, intensity: environment_map.intensity(), affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), diff --git a/crates/bevy_pbr/src/lightmap/lightmap.wgsl b/crates/bevy_pbr/src/lightmap/lightmap.wgsl index da10ece9b1..4ba6f51bc9 100644 --- a/crates/bevy_pbr/src/lightmap/lightmap.wgsl +++ b/crates/bevy_pbr/src/lightmap/lightmap.wgsl @@ -3,11 +3,11 @@ #import bevy_pbr::mesh_bindings::mesh #ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY -@group(1) @binding(4) var lightmaps_textures: binding_array, 4>; -@group(1) @binding(5) var lightmaps_samplers: binding_array; +@group(2) @binding(4) var lightmaps_textures: binding_array, 4>; +@group(2) @binding(5) var lightmaps_samplers: binding_array; #else // MULTIPLE_LIGHTMAPS_IN_ARRAY -@group(1) @binding(4) var lightmaps_texture: texture_2d; -@group(1) @binding(5) var lightmaps_sampler: sampler; +@group(2) @binding(4) var lightmaps_texture: texture_2d; +@group(2) @binding(5) var lightmaps_sampler: sampler; #endif // MULTIPLE_LIGHTMAPS_IN_ARRAY // Samples the lightmap, if any, and returns indirect illumination from it. diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index af11db1ba6..f73f1392bb 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -120,9 +120,9 @@ use tracing::error; /// In WGSL shaders, the material's binding would look like this: /// /// ```wgsl -/// @group(2) @binding(0) var color: vec4; -/// @group(2) @binding(1) var color_texture: texture_2d; -/// @group(2) @binding(2) var color_sampler: sampler; +/// @group(3) @binding(0) var color: vec4; +/// @group(3) @binding(1) var color_texture: texture_2d; +/// @group(3) @binding(2) var color_sampler: sampler; /// ``` pub trait Material: Asset + AsBindGroup + Clone + Sized { /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader @@ -501,7 +501,7 @@ where descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); } - descriptor.layout.insert(2, self.material_layout.clone()); + descriptor.layout.insert(3, self.material_layout.clone()); M::specialize(self, &mut descriptor, layout, key)?; @@ -544,8 +544,9 @@ impl FromWorld for MaterialPipeline { type DrawMaterial = ( SetItemPipeline, SetMeshViewBindGroup<0>, - SetMeshBindGroup<1>, - SetMaterialBindGroup, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup, DrawMesh, ); @@ -1410,6 +1411,7 @@ impl RenderAsset for PreparedMaterial { alpha_mask_deferred_draw_functions, material_param, ): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let draw_opaque_pbr = opaque_draw_functions.read().id::>(); let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index 735bc77c99..39028fed2d 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -2051,7 +2051,7 @@ impl MaterialDataBuffer { /// The size of the piece of data supplied to this method must equal the /// [`Self::aligned_element_size`] provided to [`MaterialDataBuffer::new`]. fn insert(&mut self, data: &[u8]) -> u32 { - // Make the the data is of the right length. + // Make sure the data is of the right length. debug_assert_eq!(data.len(), self.aligned_element_size as usize); // Grab a slot. diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 57762bfc8a..90d35d0514 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -186,13 +186,17 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( let mut shader_defs = material_fragment.shader_defs; shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); + let layout = mesh_pipeline.get_view_layout(view_key.into()); + let layout = vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + resource_manager.material_shade_bind_group_layout.clone(), + material_pipeline.material_layout.clone(), + ]; + let pipeline_descriptor = RenderPipelineDescriptor { label: material_pipeline_descriptor.label, - layout: vec![ - mesh_pipeline.get_view_layout(view_key.into()).clone(), - resource_manager.material_shade_bind_group_layout.clone(), - material_pipeline.material_layout.clone(), - ], + layout, push_constant_ranges: vec![], vertex: VertexState { shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE, diff --git a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs index 9c2d432d88..cb05de38fb 100644 --- a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs +++ b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs @@ -106,7 +106,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { render_pass.set_bind_group( 0, - &mesh_view_bind_group.value, + &mesh_view_bind_group.main, &[ view_uniform_offset.offset, view_lights_offset.offset, diff --git a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl index e179e78b7a..63e92f15e6 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl +++ b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl @@ -149,15 +149,15 @@ fn get_meshlet_vertex_position(meshlet: ptr, vertex_id: u32) #endif #ifdef MESHLET_MESH_MATERIAL_PASS -@group(1) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; -@group(1) @binding(1) var meshlet_cluster_meshlet_ids: array; // Per cluster -@group(1) @binding(2) var meshlets: array; // Per meshlet -@group(1) @binding(3) var meshlet_indices: array; // Many per meshlet -@group(1) @binding(4) var meshlet_vertex_positions: array; // Many per meshlet -@group(1) @binding(5) var meshlet_vertex_normals: array; // Many per meshlet -@group(1) @binding(6) var meshlet_vertex_uvs: array>; // Many per meshlet -@group(1) @binding(7) var meshlet_cluster_instance_ids: array; // Per cluster -@group(1) @binding(8) var meshlet_instance_uniforms: array; // Per entity instance +@group(2) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +@group(2) @binding(1) var meshlet_cluster_meshlet_ids: array; // Per cluster +@group(2) @binding(2) var meshlets: array; // Per meshlet +@group(2) @binding(3) var meshlet_indices: array; // Many per meshlet +@group(2) @binding(4) var meshlet_vertex_positions: array; // Many per meshlet +@group(2) @binding(5) var meshlet_vertex_normals: array; // Many per meshlet +@group(2) @binding(6) var meshlet_vertex_uvs: array>; // Many per meshlet +@group(2) @binding(7) var meshlet_cluster_instance_ids: array; // Per cluster +@group(2) @binding(8) var meshlet_instance_uniforms: array; // Per entity instance // TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? fn get_meshlet_vertex_id(index_id: u32) -> u32 { diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs index c25d896b8a..243bbddf22 100644 --- a/crates/bevy_pbr/src/meshlet/pipelines.rs +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -2,7 +2,7 @@ use super::resource_manager::ResourceManager; use bevy_asset::{weak_handle, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, experimental::mip_generation::DOWNSAMPLE_DEPTH_SHADER_HANDLE, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; use bevy_ecs::{ resource::Resource, @@ -80,6 +80,8 @@ impl FromWorld for MeshletPipelines { let remap_1d_to_2d_dispatch_layout = resource_manager .remap_1d_to_2d_dispatch_bind_group_layout .clone(); + + let vertex_state = world.resource::().to_vertex_state(); let pipeline_cache = world.resource_mut::(); Self { @@ -400,7 +402,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_layout], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state.clone(), primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -424,7 +426,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_shadow_view_layout], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state.clone(), primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -449,7 +451,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_resolve_material_depth_pipeline".into()), layout: vec![resolve_material_depth_layout], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state, primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: TextureFormat::Depth16Unorm, diff --git a/crates/bevy_pbr/src/meshlet/resource_manager.rs b/crates/bevy_pbr/src/meshlet/resource_manager.rs index 9b45d7676a..d33752d426 100644 --- a/crates/bevy_pbr/src/meshlet/resource_manager.rs +++ b/crates/bevy_pbr/src/meshlet/resource_manager.rs @@ -206,7 +206,7 @@ impl ResourceManager { visibility_buffer_raster_bind_group_layout: render_device.create_bind_group_layout( "meshlet_visibility_buffer_raster_bind_group_layout", &BindGroupLayoutEntries::sequential( - ShaderStages::all(), + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, ( storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), @@ -225,7 +225,7 @@ impl ResourceManager { .create_bind_group_layout( "meshlet_visibility_buffer_raster_shadow_view_bind_group_layout", &BindGroupLayoutEntries::sequential( - ShaderStages::all(), + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, ( storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), diff --git a/crates/bevy_pbr/src/parallax.rs b/crates/bevy_pbr/src/parallax.rs index 0a847b7c25..be588ca87c 100644 --- a/crates/bevy_pbr/src/parallax.rs +++ b/crates/bevy_pbr/src/parallax.rs @@ -33,6 +33,7 @@ pub enum ParallaxMappingMethod { max_steps: u32, }, } + impl ParallaxMappingMethod { /// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default. pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 }; diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 35182910a9..8732f92e82 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -95,7 +95,6 @@ where Render, prepare_prepass_view_bind_group::.in_set(RenderSystems::PrepareBindGroups), ) - .init_resource::() .init_resource::>>() .allow_ambiguous_resource::>>(); } @@ -105,7 +104,9 @@ where return; }; - render_app.init_resource::>(); + render_app + .init_resource::>() + .init_resource::(); } } @@ -216,11 +217,16 @@ pub fn update_previous_view_data( query: Query<(Entity, &Camera, &GlobalTransform), Or<(With, With)>>, ) { for (entity, camera, camera_transform) in &query { - let view_from_world = camera_transform.compute_matrix().inverse(); + let world_from_view = camera_transform.to_matrix(); + let view_from_world = world_from_view.inverse(); + let view_from_clip = camera.clip_from_view().inverse(); + commands.entity(entity).try_insert(PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view() * view_from_world, clip_from_view: camera.clip_from_view(), + world_from_clip: world_from_view * view_from_clip, + view_from_clip, }); } } @@ -267,6 +273,7 @@ pub struct PrepassPipelineInternal { pub view_layout_motion_vectors: BindGroupLayout, pub view_layout_no_motion_vectors: BindGroupLayout, pub mesh_layouts: MeshLayouts, + pub empty_layout: BindGroupLayout, pub material_layout: BindGroupLayout, pub prepass_material_vertex_shader: Option>, pub prepass_material_fragment_shader: Option>, @@ -376,6 +383,7 @@ impl FromWorld for PrepassPipeline { skins_use_uniform_buffers: skin::skins_use_uniform_buffers(render_device), depth_clip_control_supported, binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), + empty_layout: render_device.create_bind_group_layout("prepass_empty_layout", &[]), }; PrepassPipeline { internal, @@ -420,13 +428,14 @@ impl PrepassPipelineInternal { layout: &MeshVertexBufferLayoutRef, ) -> Result { let mut shader_defs = shader_defs; - let mut bind_group_layouts = vec![if mesh_key - .contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) - { - self.view_layout_motion_vectors.clone() - } else { - self.view_layout_no_motion_vectors.clone() - }]; + let mut bind_group_layouts = vec![ + if mesh_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + self.view_layout_motion_vectors.clone() + } else { + self.view_layout_no_motion_vectors.clone() + }, + self.empty_layout.clone(), + ]; let mut vertex_attributes = Vec::new(); // Let the shader code know that it's running in a prepass pipeline. @@ -551,7 +560,7 @@ impl PrepassPipelineInternal { &mut vertex_attributes, self.skins_use_uniform_buffers, ); - bind_group_layouts.insert(1, bind_group); + bind_group_layouts.insert(2, bind_group); let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; // Setup prepass fragment targets - normals in slot 0 (or None if not needed), motion vectors in slot 1 let mut targets = prepass_target_descriptors( @@ -698,11 +707,16 @@ pub fn prepare_previous_view_uniforms( let prev_view_data = match maybe_previous_view_uniforms { Some(previous_view) => previous_view.clone(), None => { - let view_from_world = camera.world_from_view.compute_matrix().inverse(); + let world_from_view = camera.world_from_view.to_matrix(); + let view_from_world = world_from_view.inverse(); + let view_from_clip = camera.clip_from_view.inverse(); + PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view * view_from_world, clip_from_view: camera.clip_from_view, + world_from_clip: world_from_view * view_from_clip, + view_from_clip, } } }; @@ -713,10 +727,29 @@ pub fn prepare_previous_view_uniforms( } } -#[derive(Default, Resource)] +#[derive(Resource)] pub struct PrepassViewBindGroup { pub motion_vectors: Option, pub no_motion_vectors: Option, + pub empty_bind_group: BindGroup, +} + +impl FromWorld for PrepassViewBindGroup { + fn from_world(world: &mut World) -> Self { + let pipeline = world.resource::>(); + + let render_device = world.resource::(); + let empty_bind_group = render_device.create_bind_group( + "prepass_view_empty_bind_group", + &pipeline.internal.empty_layout, + &[], + ); + PrepassViewBindGroup { + motion_vectors: None, + no_motion_vectors: None, + empty_bind_group, + } + } } pub fn prepare_prepass_view_bind_group( @@ -1274,7 +1307,26 @@ impl RenderCommand

for SetPrepassViewBindGroup< ); } } + RenderCommandResult::Success + } +} +pub struct SetPrepassViewEmptyBindGroup; +impl RenderCommand

for SetPrepassViewEmptyBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + _entity: Option<()>, + prepass_view_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let prepass_view_bind_group = prepass_view_bind_group.into_inner(); + pass.set_bind_group(I, &prepass_view_bind_group.empty_bind_group, &[]); RenderCommandResult::Success } } @@ -1282,7 +1334,8 @@ impl RenderCommand

for SetPrepassViewBindGroup< pub type DrawPrepass = ( SetItemPipeline, SetPrepassViewBindGroup<0>, - SetMeshBindGroup<1>, - SetMaterialBindGroup, + SetPrepassViewEmptyBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup, DrawMesh, ); diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl index 3bd27b2e03..141f7d7b0d 100644 --- a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -4,6 +4,8 @@ struct PreviousViewUniforms { view_from_world: mat4x4, clip_from_world: mat4x4, clip_from_view: mat4x4, + world_from_clip: mat4x4, + view_from_clip: mat4x4, } @group(0) @binding(2) var previous_view_uniforms: PreviousViewUniforms; diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 2592936ddd..83d28a7da7 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -548,30 +548,30 @@ pub struct LightViewEntities(EntityHashMap>); // TODO: using required component pub(crate) fn add_light_view_entities( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { + if let Ok(mut v) = commands.get_entity(trigger.target()) { v.insert(LightViewEntities::default()); } } /// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. pub(crate) fn extracted_light_removed( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { + if let Ok(mut v) = commands.get_entity(trigger.target()) { v.try_remove::(); } } pub(crate) fn remove_light_view_entities( - trigger: Trigger, + trigger: On, query: Query<&LightViewEntities>, mut commands: Commands, ) { - if let Ok(entities) = query.get(trigger.target().unwrap()) { + if let Ok(entities) = query.get(trigger.target()) { for v in entities.0.values() { for e in v.iter().copied() { if let Ok(mut v) = commands.get_entity(e) { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 8c408233e1..50d7e98a48 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1540,7 +1540,7 @@ fn extract_mesh_for_gpu_building( not_shadow_caster, no_automatic_batching, visibility_range, - ): ::Item<'_>, + ): ::Item<'_, '_>, render_visibility_ranges: &RenderVisibilityRanges, render_mesh_instances: &RenderMeshInstancesGpu, queue: &mut RenderMeshInstanceGpuQueue, @@ -1864,7 +1864,10 @@ impl MeshPipeline { } } - pub fn get_view_layout(&self, layout_key: MeshPipelineViewLayoutKey) -> &BindGroupLayout { + pub fn get_view_layout( + &self, + layout_key: MeshPipelineViewLayoutKey, + ) -> &MeshPipelineViewLayout { self.view_layouts.get_view_layout(layout_key) } } @@ -2320,7 +2323,11 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("PBR_SPECULAR_TEXTURES_SUPPORTED".into()); } - let mut bind_group_layout = vec![self.get_view_layout(key.into()).clone()]; + let bind_group_layout = self.get_view_layout(key.into()); + let mut bind_group_layout = vec![ + bind_group_layout.main_layout.clone(), + bind_group_layout.binding_array_layout.clone(), + ]; if key.msaa_samples() > 1 { shader_defs.push("MULTISAMPLED".into()); @@ -2874,7 +2881,7 @@ impl RenderCommand

for SetMeshViewBindGroup view_environment_map, mesh_view_bind_group, maybe_oit_layers_count_offset, - ): ROQueryItem<'w, Self::ViewQuery>, + ): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -2890,7 +2897,47 @@ impl RenderCommand

for SetMeshViewBindGroup if let Some(layers_count_offset) = maybe_oit_layers_count_offset { offsets.push(layers_count_offset.offset); } - pass.set_bind_group(I, &mesh_view_bind_group.value, &offsets); + pass.set_bind_group(I, &mesh_view_bind_group.main, &offsets); + + RenderCommandResult::Success + } +} + +pub struct SetMeshViewBindingArrayBindGroup; +impl RenderCommand

for SetMeshViewBindingArrayBindGroup { + type Param = (); + type ViewQuery = (Read,); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (mesh_view_bind_group,): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &mesh_view_bind_group.binding_array, &[]); + + RenderCommandResult::Success + } +} + +pub struct SetMeshViewEmptyBindGroup; +impl RenderCommand

for SetMeshViewEmptyBindGroup { + type Param = (); + type ViewQuery = (Read,); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (mesh_view_bind_group,): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &mesh_view_bind_group.empty, &[]); RenderCommandResult::Success } diff --git a/crates/bevy_pbr/src/render/mesh_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_bindings.wgsl index 62b967c56f..6e78dc4337 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_bindings.wgsl @@ -4,8 +4,8 @@ #ifndef MESHLET_MESH_MATERIAL_PASS #ifdef PER_OBJECT_BUFFER_BATCH_SIZE -@group(1) @binding(0) var mesh: array; +@group(2) @binding(0) var mesh: array; #else -@group(1) @binding(0) var mesh: array; +@group(2) @binding(0) var mesh: array; #endif // PER_OBJECT_BUFFER_BATCH_SIZE #endif // MESHLET_MESH_MATERIAL_PASS diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 8e231886ba..03165f1bc5 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -59,7 +59,9 @@ use {crate::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES, bevy_utils::once, traci #[derive(Clone)] pub struct MeshPipelineViewLayout { - pub bind_group_layout: BindGroupLayout, + pub main_layout: BindGroupLayout, + pub binding_array_layout: BindGroupLayout, + pub empty_layout: BindGroupLayout, #[cfg(debug_assertions)] pub texture_count: usize, @@ -93,24 +95,36 @@ impl MeshPipelineViewLayoutKey { format!( "mesh_view_layout{}{}{}{}{}{}", - self.contains(Key::MULTISAMPLED) - .then_some("_multisampled") - .unwrap_or_default(), - self.contains(Key::DEPTH_PREPASS) - .then_some("_depth") - .unwrap_or_default(), - self.contains(Key::NORMAL_PREPASS) - .then_some("_normal") - .unwrap_or_default(), - self.contains(Key::MOTION_VECTOR_PREPASS) - .then_some("_motion") - .unwrap_or_default(), - self.contains(Key::DEFERRED_PREPASS) - .then_some("_deferred") - .unwrap_or_default(), - self.contains(Key::OIT_ENABLED) - .then_some("_oit") - .unwrap_or_default(), + if self.contains(Key::MULTISAMPLED) { + "_multisampled" + } else { + Default::default() + }, + if self.contains(Key::DEPTH_PREPASS) { + "_depth" + } else { + Default::default() + }, + if self.contains(Key::NORMAL_PREPASS) { + "_normal" + } else { + Default::default() + }, + if self.contains(Key::MOTION_VECTOR_PREPASS) { + "_motion" + } else { + Default::default() + }, + if self.contains(Key::DEFERRED_PREPASS) { + "_deferred" + } else { + Default::default() + }, + if self.contains(Key::OIT_ENABLED) { + "_oit" + } else { + Default::default() + }, ) } } @@ -201,7 +215,11 @@ fn layout_entries( layout_key: MeshPipelineViewLayoutKey, render_device: &RenderDevice, render_adapter: &RenderAdapter, -) -> Vec { +) -> [Vec; 2] { + // EnvironmentMapLight + let environment_map_entries = + environment_map::get_bind_group_layout_entries(render_device, render_adapter); + let mut entries = DynamicBindGroupLayoutEntries::new_with_indices( ShaderStages::FRAGMENT, ( @@ -313,45 +331,15 @@ fn layout_entries( 16, texture_2d(TextureSampleType::Float { filterable: false }), ), + (17, environment_map_entries[3]), ), ); - // EnvironmentMapLight - let environment_map_entries = - environment_map::get_bind_group_layout_entries(render_device, render_adapter); - entries = entries.extend_with_indices(( - (17, environment_map_entries[0]), - (18, environment_map_entries[1]), - (19, environment_map_entries[2]), - (20, environment_map_entries[3]), - )); - - // Irradiance volumes - if IRRADIANCE_VOLUMES_ARE_USABLE { - let irradiance_volume_entries = - irradiance_volume::get_bind_group_layout_entries(render_device, render_adapter); - entries = entries.extend_with_indices(( - (21, irradiance_volume_entries[0]), - (22, irradiance_volume_entries[1]), - )); - } - - // Clustered decals - if let Some(clustered_decal_entries) = - decal::clustered::get_bind_group_layout_entries(render_device, render_adapter) - { - entries = entries.extend_with_indices(( - (23, clustered_decal_entries[0]), - (24, clustered_decal_entries[1]), - (25, clustered_decal_entries[2]), - )); - } - // Tonemapping let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); entries = entries.extend_with_indices(( - (26, tonemapping_lut_entries[0]), - (27, tonemapping_lut_entries[1]), + (18, tonemapping_lut_entries[0]), + (19, tonemapping_lut_entries[1]), )); // Prepass @@ -361,7 +349,7 @@ fn layout_entries( { for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) .iter() - .zip([28, 29, 30, 31]) + .zip([20, 21, 22, 23]) { if let Some(entry) = entry { entries = entries.extend_with_indices(((binding as u32, *entry),)); @@ -372,10 +360,10 @@ fn layout_entries( // View Transmission Texture entries = entries.extend_with_indices(( ( - 32, + 24, texture_2d(TextureSampleType::Float { filterable: true }), ), - (33, sampler(SamplerBindingType::Filtering)), + (25, sampler(SamplerBindingType::Filtering)), )); // OIT @@ -386,19 +374,47 @@ fn layout_entries( if is_oit_supported(render_adapter, render_device, false) { entries = entries.extend_with_indices(( // oit_layers - (34, storage_buffer_sized(false, None)), + (26, storage_buffer_sized(false, None)), // oit_layer_ids, - (35, storage_buffer_sized(false, None)), + (27, storage_buffer_sized(false, None)), // oit_layer_count ( - 36, + 28, uniform_buffer::(true), ), )); } } - entries.to_vec() + let mut binding_array_entries = DynamicBindGroupLayoutEntries::new(ShaderStages::FRAGMENT); + binding_array_entries = binding_array_entries.extend_with_indices(( + (0, environment_map_entries[0]), + (1, environment_map_entries[1]), + (2, environment_map_entries[2]), + )); + + // Irradiance volumes + if IRRADIANCE_VOLUMES_ARE_USABLE { + let irradiance_volume_entries = + irradiance_volume::get_bind_group_layout_entries(render_device, render_adapter); + binding_array_entries = binding_array_entries.extend_with_indices(( + (3, irradiance_volume_entries[0]), + (4, irradiance_volume_entries[1]), + )); + } + + // Clustered decals + if let Some(clustered_decal_entries) = + decal::clustered::get_bind_group_layout_entries(render_device, render_adapter) + { + binding_array_entries = binding_array_entries.extend_with_indices(( + (5, clustered_decal_entries[0]), + (6, clustered_decal_entries[1]), + (7, clustered_decal_entries[2]), + )); + } + + [entries.to_vec(), binding_array_entries.to_vec()] } /// Stores the view layouts for every combination of pipeline keys. @@ -435,12 +451,21 @@ impl FromWorld for MeshPipelineViewLayouts { #[cfg(debug_assertions)] let texture_count: usize = entries .iter() - .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + .flat_map(|e| { + e.iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + }) .count(); MeshPipelineViewLayout { - bind_group_layout: render_device - .create_bind_group_layout(key.label().as_str(), &entries), + main_layout: render_device + .create_bind_group_layout(key.label().as_str(), &entries[0]), + binding_array_layout: render_device.create_bind_group_layout( + format!("{}_binding_array", key.label()).as_str(), + &entries[1], + ), + empty_layout: render_device + .create_bind_group_layout(format!("{}_empty", key.label()).as_str(), &[]), #[cfg(debug_assertions)] texture_count, } @@ -449,7 +474,10 @@ impl FromWorld for MeshPipelineViewLayouts { } impl MeshPipelineViewLayouts { - pub fn get_view_layout(&self, layout_key: MeshPipelineViewLayoutKey) -> &BindGroupLayout { + pub fn get_view_layout( + &self, + layout_key: MeshPipelineViewLayoutKey, + ) -> &MeshPipelineViewLayout { let index = layout_key.bits() as usize; let layout = &self[index]; @@ -459,7 +487,7 @@ impl MeshPipelineViewLayouts { once!(warn!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments.")); } - &layout.bind_group_layout + layout } } @@ -484,12 +512,20 @@ pub fn generate_view_layouts( #[cfg(debug_assertions)] let texture_count: usize = entries .iter() - .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + .flat_map(|e| { + e.iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + }) .count(); MeshPipelineViewLayout { - bind_group_layout: render_device - .create_bind_group_layout(key.label().as_str(), &entries), + main_layout: render_device.create_bind_group_layout(key.label().as_str(), &entries[0]), + binding_array_layout: render_device.create_bind_group_layout( + format!("{}_binding_array", key.label()).as_str(), + &entries[1], + ), + empty_layout: render_device + .create_bind_group_layout(format!("{}_empty", key.label()).as_str(), &[]), #[cfg(debug_assertions)] texture_count, } @@ -498,7 +534,9 @@ pub fn generate_view_layouts( #[derive(Component)] pub struct MeshViewBindGroup { - pub value: BindGroup, + pub main: BindGroup, + pub binding_array: BindGroup, + pub empty: BindGroup, } pub fn prepare_mesh_view_bind_groups( @@ -585,7 +623,7 @@ pub fn prepare_mesh_view_bind_groups( layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED; } - let layout = &mesh_pipeline.get_view_layout(layout_key); + let layout = mesh_pipeline.get_view_layout(layout_key); let mut entries = DynamicBindGroupEntries::new_with_indices(( (0, view_binding.clone()), @@ -614,6 +652,58 @@ pub fn prepare_mesh_view_bind_groups( (16, ssao_view), )); + entries = entries.extend_with_indices(((17, environment_map_binding.clone()),)); + + let lut_bindings = + get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); + entries = entries.extend_with_indices(((18, lut_bindings.0), (19, lut_bindings.1))); + + // When using WebGL, we can't have a depth texture with multisampling + let prepass_bindings; + if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) || msaa.samples() == 1 + { + prepass_bindings = prepass::get_bindings(prepass_textures); + for (binding, index) in prepass_bindings + .iter() + .map(Option::as_ref) + .zip([20, 21, 22, 23]) + .flat_map(|(b, i)| b.map(|b| (b, i))) + { + entries = entries.extend_with_indices(((index, binding),)); + } + }; + + let transmission_view = transmission_texture + .map(|transmission| &transmission.view) + .unwrap_or(&fallback_image_zero.texture_view); + + let transmission_sampler = transmission_texture + .map(|transmission| &transmission.sampler) + .unwrap_or(&fallback_image_zero.sampler); + + entries = + entries.extend_with_indices(((24, transmission_view), (25, transmission_sampler))); + + if has_oit { + if let ( + Some(oit_layers_binding), + Some(oit_layer_ids_binding), + Some(oit_settings_binding), + ) = ( + oit_buffers.layers.binding(), + oit_buffers.layer_ids.binding(), + oit_buffers.settings.binding(), + ) { + entries = entries.extend_with_indices(( + (26, oit_layers_binding.clone()), + (27, oit_layer_ids_binding.clone()), + (28, oit_settings_binding.clone()), + )); + } + } + + let mut entries_binding_array = DynamicBindGroupEntries::new(); + let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( render_view_environment_maps, &images, @@ -621,18 +711,16 @@ pub fn prepare_mesh_view_bind_groups( &render_device, &render_adapter, ); - match environment_map_bind_group_entries { RenderViewEnvironmentMapBindGroupEntries::Single { diffuse_texture_view, specular_texture_view, sampler, } => { - entries = entries.extend_with_indices(( - (17, diffuse_texture_view), - (18, specular_texture_view), - (19, sampler), - (20, environment_map_binding.clone()), + entries_binding_array = entries_binding_array.extend_with_indices(( + (0, diffuse_texture_view), + (1, specular_texture_view), + (2, sampler), )); } RenderViewEnvironmentMapBindGroupEntries::Multiple { @@ -640,11 +728,10 @@ pub fn prepare_mesh_view_bind_groups( ref specular_texture_views, sampler, } => { - entries = entries.extend_with_indices(( - (17, diffuse_texture_views.as_slice()), - (18, specular_texture_views.as_slice()), - (19, sampler), - (20, environment_map_binding.clone()), + entries_binding_array = entries_binding_array.extend_with_indices(( + (0, diffuse_texture_views.as_slice()), + (1, specular_texture_views.as_slice()), + (2, sampler), )); } } @@ -666,14 +753,15 @@ pub fn prepare_mesh_view_bind_groups( texture_view, sampler, }) => { - entries = entries.extend_with_indices(((21, texture_view), (22, sampler))); + entries_binding_array = entries_binding_array + .extend_with_indices(((3, texture_view), (4, sampler))); } Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { ref texture_views, sampler, }) => { - entries = entries - .extend_with_indices(((21, texture_views.as_slice()), (22, sampler))); + entries_binding_array = entries_binding_array + .extend_with_indices(((3, texture_views.as_slice()), (4, sampler))); } None => {} } @@ -689,76 +777,42 @@ pub fn prepare_mesh_view_bind_groups( // Add the decal bind group entries. if let Some(ref render_view_decal_bind_group_entries) = decal_bind_group_entries { - entries = entries.extend_with_indices(( + entries_binding_array = entries_binding_array.extend_with_indices(( // `clustered_decals` ( - 23, + 5, render_view_decal_bind_group_entries .decals .as_entire_binding(), ), // `clustered_decal_textures` ( - 24, + 6, render_view_decal_bind_group_entries .texture_views .as_slice(), ), // `clustered_decal_sampler` - (25, render_view_decal_bind_group_entries.sampler), + (7, render_view_decal_bind_group_entries.sampler), )); } - let lut_bindings = - get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); - entries = entries.extend_with_indices(((26, lut_bindings.0), (27, lut_bindings.1))); - - // When using WebGL, we can't have a depth texture with multisampling - let prepass_bindings; - if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) || msaa.samples() == 1 - { - prepass_bindings = prepass::get_bindings(prepass_textures); - for (binding, index) in prepass_bindings - .iter() - .map(Option::as_ref) - .zip([28, 29, 30, 31]) - .flat_map(|(b, i)| b.map(|b| (b, i))) - { - entries = entries.extend_with_indices(((index, binding),)); - } - }; - - let transmission_view = transmission_texture - .map(|transmission| &transmission.view) - .unwrap_or(&fallback_image_zero.texture_view); - - let transmission_sampler = transmission_texture - .map(|transmission| &transmission.sampler) - .unwrap_or(&fallback_image_zero.sampler); - - entries = - entries.extend_with_indices(((32, transmission_view), (33, transmission_sampler))); - - if has_oit { - if let ( - Some(oit_layers_binding), - Some(oit_layer_ids_binding), - Some(oit_settings_binding), - ) = ( - oit_buffers.layers.binding(), - oit_buffers.layer_ids.binding(), - oit_buffers.settings.binding(), - ) { - entries = entries.extend_with_indices(( - (34, oit_layers_binding.clone()), - (35, oit_layer_ids_binding.clone()), - (36, oit_settings_binding.clone()), - )); - } - } - commands.entity(entity).insert(MeshViewBindGroup { - value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries), + main: render_device.create_bind_group( + "mesh_view_bind_group", + &layout.main_layout, + &entries, + ), + binding_array: render_device.create_bind_group( + "mesh_view_bind_group_binding_array", + &layout.binding_array_layout, + &entries_binding_array, + ), + empty: render_device.create_bind_group( + "mesh_view_bind_group_empty", + &layout.empty_layout, + &[], + ), }); } } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 2fb34d8466..0f650e6e54 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -50,70 +50,70 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; @group(0) @binding(15) var ssr_settings: types::ScreenSpaceReflectionsSettings; @group(0) @binding(16) var screen_space_ambient_occlusion_texture: texture_2d; - -#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(17) var diffuse_environment_maps: binding_array, 8u>; -@group(0) @binding(18) var specular_environment_maps: binding_array, 8u>; -#else -@group(0) @binding(17) var diffuse_environment_map: texture_cube; -@group(0) @binding(18) var specular_environment_map: texture_cube; -#endif -@group(0) @binding(19) var environment_map_sampler: sampler; -@group(0) @binding(20) var environment_map_uniform: types::EnvironmentMapUniform; - -#ifdef IRRADIANCE_VOLUMES_ARE_USABLE -#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(21) var irradiance_volumes: binding_array, 8u>; -#else -@group(0) @binding(21) var irradiance_volume: texture_3d; -#endif -@group(0) @binding(22) var irradiance_volume_sampler: sampler; -#endif - -#ifdef CLUSTERED_DECALS_ARE_USABLE -@group(0) @binding(23) var clustered_decals: types::ClusteredDecals; -@group(0) @binding(24) var clustered_decal_textures: binding_array, 8u>; -@group(0) @binding(25) var clustered_decal_sampler: sampler; -#endif // CLUSTERED_DECALS_ARE_USABLE +@group(0) @binding(17) var environment_map_uniform: types::EnvironmentMapUniform; // NB: If you change these, make sure to update `tonemapping_shared.wgsl` too. -@group(0) @binding(26) var dt_lut_texture: texture_3d; -@group(0) @binding(27) var dt_lut_sampler: sampler; +@group(0) @binding(18) var dt_lut_texture: texture_3d; +@group(0) @binding(19) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(28) var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(20) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(29) var normal_prepass_texture: texture_multisampled_2d; +@group(0) @binding(21) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(30) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(22) var motion_vector_prepass_texture: texture_multisampled_2d; #endif // MOTION_VECTOR_PREPASS #else // MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(28) var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(20) var depth_prepass_texture: texture_depth_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(29) var normal_prepass_texture: texture_2d; +@group(0) @binding(21) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(30) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(22) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(31) var deferred_prepass_texture: texture_2d; +@group(0) @binding(23) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(32) var view_transmission_texture: texture_2d; -@group(0) @binding(33) var view_transmission_sampler: sampler; +@group(0) @binding(24) var view_transmission_texture: texture_2d; +@group(0) @binding(25) var view_transmission_sampler: sampler; #ifdef OIT_ENABLED -@group(0) @binding(34) var oit_layers: array>; -@group(0) @binding(35) var oit_layer_ids: array>; -@group(0) @binding(36) var oit_settings: types::OrderIndependentTransparencySettings; +@group(0) @binding(26) var oit_layers: array>; +@group(0) @binding(27) var oit_layer_ids: array>; +@group(0) @binding(28) var oit_settings: types::OrderIndependentTransparencySettings; #endif // OIT_ENABLED + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY +@group(1) @binding(0) var diffuse_environment_maps: binding_array, 8u>; +@group(1) @binding(1) var specular_environment_maps: binding_array, 8u>; +#else +@group(1) @binding(0) var diffuse_environment_map: texture_cube; +@group(1) @binding(1) var specular_environment_map: texture_cube; +#endif +@group(1) @binding(2) var environment_map_sampler: sampler; + +#ifdef IRRADIANCE_VOLUMES_ARE_USABLE +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY +@group(1) @binding(3) var irradiance_volumes: binding_array, 8u>; +#else +@group(1) @binding(3) var irradiance_volume: texture_3d; +#endif +@group(1) @binding(4) var irradiance_volume_sampler: sampler; +#endif + +#ifdef CLUSTERED_DECALS_ARE_USABLE +@group(1) @binding(5) var clustered_decals: types::ClusteredDecals; +@group(1) @binding(6) var clustered_decal_textures: binding_array, 8u>; +@group(1) @binding(7) var clustered_decal_sampler: sampler; +#endif // CLUSTERED_DECALS_ARE_USABLE diff --git a/crates/bevy_pbr/src/render/morph.wgsl b/crates/bevy_pbr/src/render/morph.wgsl index 939b714c77..6689d68cc6 100644 --- a/crates/bevy_pbr/src/render/morph.wgsl +++ b/crates/bevy_pbr/src/render/morph.wgsl @@ -4,9 +4,9 @@ #import bevy_pbr::mesh_types::MorphWeights; -@group(1) @binding(2) var morph_weights: MorphWeights; -@group(1) @binding(3) var morph_targets: texture_3d; -@group(1) @binding(7) var prev_morph_weights: MorphWeights; +@group(2) @binding(2) var morph_weights: MorphWeights; +@group(2) @binding(3) var morph_targets: texture_3d; +@group(2) @binding(7) var prev_morph_weights: MorphWeights; // NOTE: Those are the "hardcoded" values found in `MorphAttributes` struct // in crates/bevy_render/src/mesh/morph/visitors.rs diff --git a/crates/bevy_pbr/src/render/pbr_bindings.wgsl b/crates/bevy_pbr/src/render/pbr_bindings.wgsl index fac7b97265..373d0bdd54 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -37,53 +37,53 @@ struct StandardMaterialBindings { specular_tint_sampler: u32, // 30 } -@group(2) @binding(0) var material_indices: array; -@group(2) @binding(10) var material_array: array; +@group(3) @binding(0) var material_indices: array; +@group(3) @binding(10) var material_array: array; #else // BINDLESS -@group(2) @binding(0) var material: StandardMaterial; -@group(2) @binding(1) var base_color_texture: texture_2d; -@group(2) @binding(2) var base_color_sampler: sampler; -@group(2) @binding(3) var emissive_texture: texture_2d; -@group(2) @binding(4) var emissive_sampler: sampler; -@group(2) @binding(5) var metallic_roughness_texture: texture_2d; -@group(2) @binding(6) var metallic_roughness_sampler: sampler; -@group(2) @binding(7) var occlusion_texture: texture_2d; -@group(2) @binding(8) var occlusion_sampler: sampler; -@group(2) @binding(9) var normal_map_texture: texture_2d; -@group(2) @binding(10) var normal_map_sampler: sampler; -@group(2) @binding(11) var depth_map_texture: texture_2d; -@group(2) @binding(12) var depth_map_sampler: sampler; +@group(3) @binding(0) var material: StandardMaterial; +@group(3) @binding(1) var base_color_texture: texture_2d; +@group(3) @binding(2) var base_color_sampler: sampler; +@group(3) @binding(3) var emissive_texture: texture_2d; +@group(3) @binding(4) var emissive_sampler: sampler; +@group(3) @binding(5) var metallic_roughness_texture: texture_2d; +@group(3) @binding(6) var metallic_roughness_sampler: sampler; +@group(3) @binding(7) var occlusion_texture: texture_2d; +@group(3) @binding(8) var occlusion_sampler: sampler; +@group(3) @binding(9) var normal_map_texture: texture_2d; +@group(3) @binding(10) var normal_map_sampler: sampler; +@group(3) @binding(11) var depth_map_texture: texture_2d; +@group(3) @binding(12) var depth_map_sampler: sampler; #ifdef PBR_ANISOTROPY_TEXTURE_SUPPORTED -@group(2) @binding(13) var anisotropy_texture: texture_2d; -@group(2) @binding(14) var anisotropy_sampler: sampler; +@group(3) @binding(13) var anisotropy_texture: texture_2d; +@group(3) @binding(14) var anisotropy_sampler: sampler; #endif // PBR_ANISOTROPY_TEXTURE_SUPPORTED #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED -@group(2) @binding(15) var specular_transmission_texture: texture_2d; -@group(2) @binding(16) var specular_transmission_sampler: sampler; -@group(2) @binding(17) var thickness_texture: texture_2d; -@group(2) @binding(18) var thickness_sampler: sampler; -@group(2) @binding(19) var diffuse_transmission_texture: texture_2d; -@group(2) @binding(20) var diffuse_transmission_sampler: sampler; +@group(3) @binding(15) var specular_transmission_texture: texture_2d; +@group(3) @binding(16) var specular_transmission_sampler: sampler; +@group(3) @binding(17) var thickness_texture: texture_2d; +@group(3) @binding(18) var thickness_sampler: sampler; +@group(3) @binding(19) var diffuse_transmission_texture: texture_2d; +@group(3) @binding(20) var diffuse_transmission_sampler: sampler; #endif // PBR_TRANSMISSION_TEXTURES_SUPPORTED #ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED -@group(2) @binding(21) var clearcoat_texture: texture_2d; -@group(2) @binding(22) var clearcoat_sampler: sampler; -@group(2) @binding(23) var clearcoat_roughness_texture: texture_2d; -@group(2) @binding(24) var clearcoat_roughness_sampler: sampler; -@group(2) @binding(25) var clearcoat_normal_texture: texture_2d; -@group(2) @binding(26) var clearcoat_normal_sampler: sampler; +@group(3) @binding(21) var clearcoat_texture: texture_2d; +@group(3) @binding(22) var clearcoat_sampler: sampler; +@group(3) @binding(23) var clearcoat_roughness_texture: texture_2d; +@group(3) @binding(24) var clearcoat_roughness_sampler: sampler; +@group(3) @binding(25) var clearcoat_normal_texture: texture_2d; +@group(3) @binding(26) var clearcoat_normal_sampler: sampler; #endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED #ifdef PBR_SPECULAR_TEXTURES_SUPPORTED -@group(2) @binding(27) var specular_texture: texture_2d; -@group(2) @binding(28) var specular_sampler: sampler; -@group(2) @binding(29) var specular_tint_texture: texture_2d; -@group(2) @binding(30) var specular_tint_sampler: sampler; +@group(3) @binding(27) var specular_texture: texture_2d; +@group(3) @binding(28) var specular_sampler: sampler; +@group(3) @binding(29) var specular_tint_texture: texture_2d; +@group(3) @binding(30) var specular_tint_sampler: sampler; #endif // PBR_SPECULAR_TEXTURES_SUPPORTED #endif // BINDLESS diff --git a/crates/bevy_pbr/src/render/skinning.wgsl b/crates/bevy_pbr/src/render/skinning.wgsl index 1762a73887..6c4da0754a 100644 --- a/crates/bevy_pbr/src/render/skinning.wgsl +++ b/crates/bevy_pbr/src/render/skinning.wgsl @@ -6,9 +6,9 @@ #ifdef SKINNED #ifdef SKINS_USE_UNIFORM_BUFFERS -@group(1) @binding(1) var joint_matrices: SkinnedMesh; +@group(2) @binding(1) var joint_matrices: SkinnedMesh; #else // SKINS_USE_UNIFORM_BUFFERS -@group(1) @binding(1) var joint_matrices: array>; +@group(2) @binding(1) var joint_matrices: array>; #endif // SKINS_USE_UNIFORM_BUFFERS // An array of matrices specifying the joint positions from the previous frame. @@ -18,9 +18,9 @@ // If this is the first frame, or we're otherwise prevented from using data from // the previous frame, this is simply the same as `joint_matrices` above. #ifdef SKINS_USE_UNIFORM_BUFFERS -@group(1) @binding(6) var prev_joint_matrices: SkinnedMesh; +@group(2) @binding(6) var prev_joint_matrices: SkinnedMesh; #else // SKINS_USE_UNIFORM_BUFFERS -@group(1) @binding(6) var prev_joint_matrices: array>; +@group(2) @binding(6) var prev_joint_matrices: array>; #endif // SKINS_USE_UNIFORM_BUFFERS fn skin_model( diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 9f7dbb2f76..f3d876ffde 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -7,8 +7,8 @@ use bevy_core_pipeline::{ graph::{Core3d, Node3d}, DEPTH_TEXTURE_SAMPLING_SUPPORTED, }, - fullscreen_vertex_shader, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + FullscreenShader, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -155,7 +155,8 @@ pub struct ScreenSpaceReflectionsPipeline { depth_nearest_sampler: Sampler, bind_group_layout: BindGroupLayout, binding_arrays_are_usable: bool, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// A GPU buffer that stores the screen space reflection settings for each view. @@ -280,7 +281,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { view_environment_map_offset, view_bind_group, ssr_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // Grab the render pipeline. @@ -322,7 +323,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { render_pass.set_render_pipeline(render_pipeline); render_pass.set_bind_group( 0, - &view_bind_group.value, + &view_bind_group.main, &[ view_uniform_offset.offset, view_lights_offset.offset, @@ -332,9 +333,10 @@ impl ViewNode for ScreenSpaceReflectionsNode { **view_environment_map_offset, ], ); + render_pass.set_bind_group(1, &view_bind_group.binding_array, &[]); // Perform the SSR render pass. - render_pass.set_bind_group(1, &ssr_bind_group, &[]); + render_pass.set_bind_group(2, &ssr_bind_group, &[]); render_pass.draw(0..3, 0..1); Ok(()) @@ -397,9 +399,10 @@ impl FromWorld for ScreenSpaceReflectionsPipeline { depth_nearest_sampler, bind_group_layout, binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), + fullscreen_shader: world.resource::().clone(), // Even though ssr was loaded using load_shader_library, we can still access it like a // normal embedded asset (so we can use it as both a library or a kernel). - shader: load_embedded_asset!(world, "ssr.wgsl"), + fragment_shader: load_embedded_asset!(world, "ssr.wgsl"), } } } @@ -498,7 +501,7 @@ impl ExtractComponent for ScreenSpaceReflections { type Out = ScreenSpaceReflectionsUniform; - fn extract_component(settings: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { once!(info!( "Disabling screen-space reflections on this platform because depth textures \ @@ -515,9 +518,14 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { type Key = ScreenSpaceReflectionsPipelineKey; fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { - let mesh_view_layout = self + let layout = self .mesh_view_layouts .get_view_layout(key.mesh_pipeline_view_key); + let layout = vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + self.bind_group_layout.clone(), + ]; let mut shader_defs = vec![ "DEPTH_PREPASS".into(), @@ -535,10 +543,10 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { RenderPipelineDescriptor { label: Some("SSR pipeline".into()), - layout: vec![mesh_view_layout.clone(), self.bind_group_layout.clone()], - vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + layout, + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_pbr/src/ssr/raymarch.wgsl b/crates/bevy_pbr/src/ssr/raymarch.wgsl index e149edfbbc..12140c91e3 100644 --- a/crates/bevy_pbr/src/ssr/raymarch.wgsl +++ b/crates/bevy_pbr/src/ssr/raymarch.wgsl @@ -25,10 +25,10 @@ } // Allows us to sample from the depth buffer with bilinear filtering. -@group(1) @binding(2) var depth_linear_sampler: sampler; +@group(2) @binding(2) var depth_linear_sampler: sampler; // Allows us to sample from the depth buffer with nearest-neighbor filtering. -@group(1) @binding(3) var depth_nearest_sampler: sampler; +@group(2) @binding(3) var depth_nearest_sampler: sampler; // Main code diff --git a/crates/bevy_pbr/src/ssr/ssr.wgsl b/crates/bevy_pbr/src/ssr/ssr.wgsl index 3dddfa1ba3..d646ac69fe 100644 --- a/crates/bevy_pbr/src/ssr/ssr.wgsl +++ b/crates/bevy_pbr/src/ssr/ssr.wgsl @@ -36,10 +36,10 @@ #endif // The texture representing the color framebuffer. -@group(1) @binding(0) var color_texture: texture_2d; +@group(2) @binding(0) var color_texture: texture_2d; // The sampler that lets us sample from the color framebuffer. -@group(1) @binding(1) var color_sampler: sampler; +@group(2) @binding(1) var color_sampler: sampler; // Group 1, bindings 2 and 3 are in `raymarch.wgsl`. diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 45f694e546..a5cd8e56f3 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -347,7 +347,7 @@ impl ViewNode for VolumetricFogNode { view_ssr_offset, msaa, view_environment_map_offset, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -461,7 +461,7 @@ impl ViewNode for VolumetricFogNode { render_pass.set_pipeline(pipeline); render_pass.set_bind_group( 0, - &view_bind_group.value, + &view_bind_group.main, &[ view_uniform_offset.offset, view_lights_offset.offset, @@ -511,10 +511,6 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { type Key = VolumetricFogPipelineKey; fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { - let mesh_view_layout = self - .mesh_view_layouts - .get_view_layout(key.mesh_pipeline_view_key); - // We always use hardware 2x2 filtering for sampling the shadow map; the // more accurate versions with percentage-closer filtering aren't worth // the overhead. @@ -559,9 +555,17 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { shader_defs.push("DENSITY_TEXTURE".into()); } + let layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + let layout = vec![ + layout.main_layout.clone(), + volumetric_view_bind_group_layout.clone(), + ]; + RenderPipelineDescriptor { label: Some("volumetric lighting pipeline".into()), - layout: vec![mesh_view_layout.clone(), volumetric_view_bind_group_layout], + layout, push_constant_ranges: vec![], vertex: VertexState { shader: self.shader.clone(), @@ -700,7 +704,7 @@ pub fn prepare_volumetric_fog_uniforms( // Do this up front to avoid O(n^2) matrix inversion. local_from_world_matrices.clear(); for (_, _, fog_transform) in fog_volumes.iter() { - local_from_world_matrices.push(fog_transform.compute_matrix().inverse()); + local_from_world_matrices.push(fog_transform.to_matrix().inverse()); } let uniform_count = view_targets.iter().len() * local_from_world_matrices.len(); @@ -712,7 +716,7 @@ pub fn prepare_volumetric_fog_uniforms( }; for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() { - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let mut view_fog_volumes = vec![]; diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index cc64ad2e4f..f7c193161d 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -1,6 +1,7 @@ use crate::{ DrawMesh, MeshPipeline, MeshPipelineKey, RenderMeshInstanceFlags, RenderMeshInstances, - SetMeshBindGroup, SetMeshViewBindGroup, ViewKeyCache, ViewSpecializationTicks, + SetMeshBindGroup, SetMeshViewBindGroup, SetMeshViewBindingArrayBindGroup, ViewKeyCache, + ViewSpecializationTicks, }; use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ @@ -318,7 +319,8 @@ impl RenderCommand

for SetWireframe3dPushConstants { pub type DrawWireframe3d = ( SetItemPipeline, SetMeshViewBindGroup<0>, - SetMeshBindGroup<1>, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, SetWireframe3dPushConstants, DrawMesh, ); @@ -374,7 +376,7 @@ impl ViewNode for Wireframe3dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(wireframe_phase) = world.get_resource::>() @@ -474,6 +476,7 @@ impl RenderAsset for RenderWireframeMaterial { source_asset: Self::SourceAsset, _asset_id: AssetId, _param: &mut SystemParamItem, + _previous_asset: Option<&Self>, ) -> Result> { Ok(RenderWireframeMaterial { color: source_asset.color.to_linear().to_f32_array(), diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index b42f0db14c..ac18b1dd89 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_picking" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides screen picking functionality for Bevy Engine" homepage = "https://bevy.org" @@ -13,20 +13,20 @@ bevy_mesh_picking_backend = ["dep:bevy_mesh", "dep:crossbeam-channel"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev", optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 9e28cc6d7c..28693314d9 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -55,7 +55,7 @@ pub mod prelude { /// Note that systems reading these events in [`PreUpdate`](bevy_app::PreUpdate) will not report ordering /// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered /// against [`PickingSystems::Backend`](crate::PickingSystems::Backend), or better, avoid reading `PointerHits` in `PreUpdate`. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct PointerHits { /// The pointer associated with this hit test. diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 72c0f06c46..a7a3979c59 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -11,7 +11,7 @@ //! # use bevy_picking::prelude::*; //! # let mut world = World::default(); //! world.spawn_empty() -//! .observe(|trigger: Trigger>| { +//! .observe(|trigger: On>| { //! println!("I am being hovered over"); //! }); //! ``` @@ -31,7 +31,7 @@ //! //! The events this module defines fall into a few broad categories: //! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. -//! + Clicking and pressing: [`Pressed`], [`Released`], and [`Click`]. +//! + Clicking and pressing: [`Press`], [`Release`], and [`Click`]. //! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. //! //! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains @@ -59,11 +59,10 @@ use crate::{ /// /// The documentation for the [`pointer_events`] explains the events this module exposes and /// the order in which they fire. -#[derive(Clone, PartialEq, Debug, Reflect, Component)] +#[derive(Event, BufferedEvent, EntityEvent, Clone, PartialEq, Debug, Reflect, Component)] +#[entity_event(traversal = PointerTraversal, auto_propagate)] #[reflect(Component, Debug, Clone)] pub struct Pointer { - /// The original target of this picking event, before bubbling - pub target: Entity, /// The pointer that triggered this event pub pointer_id: PointerId, /// The location of the pointer during this event @@ -87,7 +86,7 @@ impl Traversal> for PointerTraversal where E: Debug + Clone + Reflect, { - fn traverse(item: Self::Item<'_>, pointer: &Pointer) -> Option { + fn traverse(item: Self::Item<'_, '_>, pointer: &Pointer) -> Option { let PointerTraversalItem { child_of, window } = item; // Send event to parent, if it has one. @@ -106,15 +105,6 @@ where } } -impl Event for Pointer -where - E: Debug + Clone + Reflect, -{ - type Traversal = PointerTraversal; - - const AUTO_PROPAGATE: bool = true; -} - impl core::fmt::Display for Pointer { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( @@ -134,9 +124,8 @@ impl core::ops::Deref for Pointer { impl Pointer { /// Construct a new `Pointer` event. - pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self { + pub fn new(id: PointerId, location: Location, event: E) -> Self { Self { - target, pointer_id: id, pointer_location: location, event, @@ -171,7 +160,7 @@ pub struct Out { /// Fires when a pointer button is pressed over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Pressed { +pub struct Press { /// Pointer button pressed to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -181,7 +170,7 @@ pub struct Pressed { /// Fires when a pointer button is released over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Released { +pub struct Release { /// Pointer button lifted to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -400,7 +389,7 @@ impl PointerState { pub struct PickingEventWriters<'w> { cancel_events: EventWriter<'w, Pointer>, click_events: EventWriter<'w, Pointer>, - pressed_events: EventWriter<'w, Pointer>, + pressed_events: EventWriter<'w, Pointer>, drag_drop_events: EventWriter<'w, Pointer>, drag_end_events: EventWriter<'w, Pointer>, drag_enter_events: EventWriter<'w, Pointer>, @@ -412,7 +401,7 @@ pub struct PickingEventWriters<'w> { move_events: EventWriter<'w, Pointer>, out_events: EventWriter<'w, Pointer>, over_events: EventWriter<'w, Pointer>, - released_events: EventWriter<'w, Pointer>, + released_events: EventWriter<'w, Pointer>, } /// Dispatches interaction events to the target entities. @@ -422,7 +411,7 @@ pub struct PickingEventWriters<'w> { /// + [`DragEnter`] → [`Over`]. /// + Any number of any of the following: /// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`]. -/// + For each button press: [`Pressed`] or [`Click`] → [`Released`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. +/// + For each button press: [`Press`] or [`Click`] → [`Release`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. /// + For each pointer cancellation: [`Cancel`]. /// /// Additionally, across multiple frames, the following are also strictly @@ -430,7 +419,7 @@ pub struct PickingEventWriters<'w> { /// + When a pointer moves over the target: /// [`Over`], [`Move`], [`Out`]. /// + When a pointer presses buttons on the target: -/// [`Pressed`], [`Click`], [`Released`]. +/// [`Press`], [`Click`], [`Release`]. /// + When a pointer drags the target: /// [`DragStart`], [`Drag`], [`DragEnd`]. /// + When a pointer drags something over the target: @@ -452,7 +441,7 @@ pub struct PickingEventWriters<'w> { /// In the context of UI, this is especially problematic. Additional hierarchy-aware /// events will be added in a future release. /// -/// Both [`Click`] and [`Released`] target the entity hovered in the *previous frame*, +/// Both [`Click`] and [`Release`] target the entity hovered in the *previous frame*, /// rather than the current frame. This is because touch pointers hover nothing /// on the frame they are released. The end effect is that these two events can /// be received sequentially after an [`Out`] event (but always on the same frame @@ -505,12 +494,7 @@ pub fn pointer_events( }; // Always send Out events - let out_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Out { hit: hit.clone() }, - ); + let out_event = Pointer::new(pointer_id, location.clone(), Out { hit: hit.clone() }); commands.trigger_targets(out_event.clone(), hovered_entity); event_writers.out_events.write(out_event); @@ -522,7 +506,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragLeave { button, dragged: *drag_target, @@ -564,7 +547,6 @@ pub fn pointer_events( let drag_enter_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragEnter { button, dragged: *drag_target, @@ -577,12 +559,7 @@ pub fn pointer_events( } // Always send Over events - let over_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Over { hit: hit.clone() }, - ); + let over_event = Pointer::new(pointer_id, location.clone(), Over { hit: hit.clone() }); commands.trigger_targets(over_event.clone(), hovered_entity); event_writers.over_events.write(over_event); } @@ -608,8 +585,7 @@ pub fn pointer_events( let pressed_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, - Pressed { + Press { button, hit: hit.clone(), }, @@ -636,7 +612,6 @@ pub fn pointer_events( let click_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Click { button, hit: hit.clone(), @@ -646,12 +621,11 @@ pub fn pointer_events( commands.trigger_targets(click_event.clone(), hovered_entity); event_writers.click_events.write(click_event); } - // Always send the Released event + // Always send the Release event let released_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, - Released { + Release { button, hit: hit.clone(), }, @@ -667,7 +641,6 @@ pub fn pointer_events( let drag_drop_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragDrop { button, dropped: drag_target, @@ -681,7 +654,6 @@ pub fn pointer_events( let drag_end_event = Pointer::new( pointer_id, location.clone(), - drag_target, DragEnd { button, distance: drag.latest_pos - drag.start_pos, @@ -694,7 +666,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragLeave { button, dragged: drag_target, @@ -735,7 +706,6 @@ pub fn pointer_events( let drag_start_event = Pointer::new( pointer_id, location.clone(), - *press_target, DragStart { button, hit: hit.clone(), @@ -754,7 +724,6 @@ pub fn pointer_events( let drag_event = Pointer::new( pointer_id, location.clone(), - *drag_target, Drag { button, distance: location.position - drag.start_pos, @@ -777,7 +746,6 @@ pub fn pointer_events( let drag_over_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragOver { button, dragged: *drag_target, @@ -799,7 +767,6 @@ pub fn pointer_events( let move_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Move { hit: hit.clone(), delta, @@ -819,7 +786,6 @@ pub fn pointer_events( let scroll_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Scroll { unit, x, @@ -839,8 +805,7 @@ pub fn pointer_events( .iter() .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) { - let cancel_event = - Pointer::new(pointer_id, location.clone(), hovered_entity, Cancel { hit }); + let cancel_event = Pointer::new(pointer_id, location.clone(), Cancel { hit }); commands.trigger_targets(cancel_event.clone(), hovered_entity); event_writers.cancel_events.write(cancel_event); } diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 2bf23c50ba..dbb6ee942e 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -14,7 +14,7 @@ use crate::{ }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::*; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::prelude::*; @@ -279,3 +279,285 @@ fn merge_interaction_states( new_interaction_state.insert(*hovered_entity, new_interaction); } } + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// enters or leaves an entity. Users should insert this component on an entity to indicate interest +/// in knowing about hover state changes. +/// +/// The component's boolean value will be `true` whenever the pointer is currently directly hovering +/// over the entity, or any of the entity's descendants (as defined by the [`ChildOf`] +/// relationship). This is consistent with the behavior of the CSS `:hover` pseudo-class, which +/// applies to the element and all of its descendants. +/// +/// The contained boolean value is guaranteed to only be mutated when the pointer enters or leaves +/// the entity, allowing Bevy change detection to be used efficiently. This is in contrast to the +/// [`HoverMap`] resource, which is updated every frame. +/// +/// Typically, a simple hoverable entity or widget will have this component added to it. More +/// complex widgets can have this component added to each hoverable part. +/// +/// The computational cost of keeping the `Hovered` components up to date is relatively cheap, and +/// linear in the number of entities that have the [`Hovered`] component inserted. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct Hovered(pub bool); + +impl Hovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// is directly hovering over an entity. Users should insert this component on an entity to indicate +/// interest in knowing about hover state changes. +/// +/// This is similar to [`Hovered`] component, except that it does not include descendants in the +/// hover state. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct DirectlyHovered(pub bool); + +impl DirectlyHovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// Uses [`HoverMap`] changes to update [`Hovered`] components. +pub fn update_is_hovered( + hover_map: Option>, + mut hovers: Query<(Entity, &Hovered)>, + parent_query: Query<&ChildOf>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + // Algorithm: for each entity having a `Hovered` component, we want to know if the current + // entry in the hover map is "within" (that is, in the set of descenants of) that entity. Rather + // than doing an expensive breadth-first traversal of children, instead start with the hovermap + // entry and search upwards. We can make this even cheaper by building a set of ancestors for + // the hovermap entry, and then testing each `Hovered` entity against that set. + + // A set which contains the hovered for the current pointer entity and its ancestors. The + // capacity is based on the likely tree depth of the hierarchy, which is typically greater for + // UI (because of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound + // for most use cases. + let mut hover_ancestors = EntityHashSet::with_capacity(32); + if let Some(map) = hover_map.get(&PointerId::Mouse) { + for hovered_entity in map.keys() { + hover_ancestors.insert(*hovered_entity); + hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity)); + } + } + + // For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors. + for (entity, hoverable) in hovers.iter_mut() { + let is_hovering = hover_ancestors.contains(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(Hovered(is_hovering)); + } + } +} + +/// Uses [`HoverMap`] changes to update [`DirectlyHovered`] components. +pub fn update_is_directly_hovered( + hover_map: Option>, + hovers: Query<(Entity, &DirectlyHovered)>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + if let Some(map) = hover_map.get(&PointerId::Mouse) { + // It's hovering if it's in the HoverMap. + for (entity, hoverable) in hovers.iter() { + let is_hovering = map.contains_key(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(DirectlyHovered(is_hovering)); + } + } + } else { + // No hovered entity, reset all hovers. + for (entity, hoverable) in hovers.iter() { + if hoverable.0 { + commands.entity(entity).insert(DirectlyHovered(false)); + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy_render::camera::Camera; + + use super::*; + + #[test] + fn update_is_hovered_memoized() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_hovered).is_ok()); + + // Check to insure that the hovered entity has the Hovered component set to true + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_self() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_entity = world.spawn(DirectlyHovered(false)).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_entity, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the hovered entity has the DirectlyHovered component set to true + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_child() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world + .spawn(DirectlyHovered(false)) + .add_child(hovered_child) + .id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the DirectlyHovered component is still false + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 70a5714581..74a765fbcd 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -17,7 +17,7 @@ //! # struct MyComponent; //! # let mut world = World::new(); //! world.spawn(MyComponent) -//! .observe(|mut trigger: Trigger>| { +//! .observe(|mut trigger: On>| { //! println!("I was just clicked!"); //! // Get the underlying pointer event data //! let click_event: &Pointer = trigger.event(); @@ -39,7 +39,7 @@ //! //! When events are generated, they bubble up the entity hierarchy starting from their target, until //! they reach the root or bubbling is halted with a call to -//! [`Trigger::propagate`](bevy_ecs::observer::Trigger::propagate). See [`Observer`] for details. +//! [`On::propagate`](bevy_ecs::observer::On::propagate). See [`Observer`] for details. //! //! This allows you to run callbacks when any children of an entity are interacted with, and leads //! to succinct, expressive code: @@ -48,22 +48,22 @@ //! # use bevy_ecs::prelude::*; //! # use bevy_transform::prelude::*; //! # use bevy_picking::prelude::*; -//! # #[derive(Event)] +//! # #[derive(Event, BufferedEvent)] //! # struct Greeting; //! fn setup(mut commands: Commands) { //! commands.spawn(Transform::default()) -//! // Spawn your entity here, e.g. a Mesh. +//! // Spawn your entity here, e.g. a `Mesh3d`. //! // When dragged, mutate the `Transform` component on the dragged target entity: -//! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { -//! let mut transform = transforms.get_mut(trigger.target().unwrap()).unwrap(); +//! .observe(|trigger: On>, mut transforms: Query<&mut Transform>| { +//! let mut transform = transforms.get_mut(trigger.target()).unwrap(); //! let drag = trigger.event(); //! transform.rotate_local_y(drag.delta.x / 50.0); //! }) -//! .observe(|trigger: Trigger>, mut commands: Commands| { -//! println!("Entity {} goes BOOM!", trigger.target().unwrap()); -//! commands.entity(trigger.target().unwrap()).despawn(); +//! .observe(|trigger: On>, mut commands: Commands| { +//! println!("Entity {} goes BOOM!", trigger.target()); +//! commands.entity(trigger.target()).despawn(); //! }) -//! .observe(|trigger: Trigger>, mut events: EventWriter| { +//! .observe(|trigger: On>, mut events: EventWriter| { //! events.write(Greeting); //! }); //! } @@ -170,6 +170,7 @@ pub mod window; use bevy_app::{prelude::*, PluginGroupBuilder}; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +use hover::{update_is_directly_hovered, update_is_hovered}; /// The picking prelude. /// @@ -285,7 +286,7 @@ pub type PickSet = PickingSystems; /// /// Note: for any of these plugins to work, they require a picking backend to be active, /// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] -/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. +/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::On`]s. #[derive(Default)] pub struct DefaultPickingPlugins; @@ -392,6 +393,7 @@ impl Plugin for PickingPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -414,7 +416,7 @@ impl Plugin for InteractionPlugin { .init_resource::() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_event::>() .add_event::>() @@ -425,11 +427,16 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_systems( PreUpdate, - (generate_hovermap, update_interactions, pointer_events) + ( + generate_hovermap, + update_interactions, + (update_is_hovered, update_is_directly_hovered), + pointer_events, + ) .chain() .in_set(PickingSystems::Hover), ); diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index 9988a96e19..d521fe1213 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -1,5 +1,5 @@ -use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec3, Vec3A}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec2, Vec3, Vec3A}; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; use bevy_reflect::Reflect; use super::Backfaces; @@ -18,6 +18,8 @@ pub struct RayMeshHit { pub distance: f32, /// The vertices of the triangle that was hit. pub triangle: Option<[Vec3; 3]>, + /// UV coordinate of the hit, if the mesh has UV attributes. + pub uv: Option, /// The index of the triangle that was hit. pub triangle_index: Option, } @@ -26,6 +28,10 @@ pub struct RayMeshHit { #[derive(Default, Debug)] pub struct RayTriangleHit { pub distance: f32, + /// Note this uses the convention from the Moller-Trumbore algorithm: + /// P = (1 - u - v)A + uB + vC + /// This is different from the more common convention of + /// P = uA + vB + (1 - u - v)C pub barycentric_coords: (f32, f32), } @@ -34,7 +40,7 @@ pub(super) fn ray_intersection_over_mesh( mesh: &Mesh, transform: &Mat4, ray: Ray3d, - culling: Backfaces, + cull: Backfaces, ) -> Option { if mesh.primitive_topology() != PrimitiveTopology::TriangleList { return None; // ray_mesh_intersection assumes vertices are laid out in a triangle list @@ -47,26 +53,37 @@ pub(super) fn ray_intersection_over_mesh( .attribute(Mesh::ATTRIBUTE_NORMAL) .and_then(|normal_values| normal_values.as_float3()); + let uvs = mesh + .attribute(Mesh::ATTRIBUTE_UV_0) + .and_then(|uvs| match uvs { + VertexAttributeValues::Float32x2(uvs) => Some(uvs.as_slice()), + _ => None, + }); + match mesh.indices() { Some(Indices::U16(indices)) => { - ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling) + ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull) } Some(Indices::U32(indices)) => { - ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling) + ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull) } - None => ray_mesh_intersection::(ray, transform, positions, normals, None, culling), + None => ray_mesh_intersection::(ray, transform, positions, normals, None, uvs, cull), } } /// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. -pub fn ray_mesh_intersection + Clone + Copy>( +pub fn ray_mesh_intersection( ray: Ray3d, mesh_transform: &Mat4, positions: &[[f32; 3]], vertex_normals: Option<&[[f32; 3]]>, indices: Option<&[I]>, + uvs: Option<&[[f32; 2]]>, backface_culling: Backfaces, -) -> Option { +) -> Option +where + I: TryInto + Clone + Copy, +{ let world_to_mesh = mesh_transform.inverse(); let ray = Ray3d::new( @@ -139,17 +156,12 @@ pub fn ray_mesh_intersection + Clone + Copy>( closest_hit.and_then(|(tri_idx, hit)| { let [a, b, c] = match indices { Some(indices) => { - let triangle = indices.get((tri_idx * 3)..(tri_idx * 3 + 3))?; - - let [Ok(a), Ok(b), Ok(c)] = [ - triangle[0].try_into(), - triangle[1].try_into(), - triangle[2].try_into(), - ] else { - return None; - }; - - [a, b, c] + let [i, j, k] = [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2]; + [ + indices.get(i).copied()?.try_into().ok()?, + indices.get(j).copied()?.try_into().ok()?, + indices.get(k).copied()?.try_into().ok()?, + ] } None => [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2], }; @@ -168,10 +180,12 @@ pub fn ray_mesh_intersection + Clone + Copy>( }); let point = ray.get_point(hit.distance); + // Note that we need to convert from the Möller-Trumbore convention to the more common + // P = uA + vB + (1 - u - v)C convention. let u = hit.barycentric_coords.0; let v = hit.barycentric_coords.1; let w = 1.0 - u - v; - let barycentric = Vec3::new(u, v, w); + let barycentric = Vec3::new(w, u, v); let normal = if let Some(normals) = tri_normals { normals[1] * u + normals[2] * v + normals[0] * w @@ -181,9 +195,29 @@ pub fn ray_mesh_intersection + Clone + Copy>( .normalize() }; + let uv = uvs.and_then(|uvs| { + let tri_uvs = if let Some(indices) = indices { + let i = tri_idx * 3; + [ + uvs[indices[i].try_into().ok()?], + uvs[indices[i + 1].try_into().ok()?], + uvs[indices[i + 2].try_into().ok()?], + ] + } else { + let i = tri_idx * 3; + [uvs[i], uvs[i + 1], uvs[i + 2]] + }; + Some( + barycentric.x * Vec2::from(tri_uvs[0]) + + barycentric.y * Vec2::from(tri_uvs[1]) + + barycentric.z * Vec2::from(tri_uvs[2]), + ) + }); + Some(RayMeshHit { point: mesh_transform.transform_point3(point), normal: mesh_transform.transform_vector3(normal), + uv, barycentric_coords: barycentric, distance: mesh_transform .transform_vector3(ray.direction * hit.distance) @@ -317,7 +351,7 @@ mod tests { #[test] fn ray_mesh_intersection_simple() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = None; @@ -329,6 +363,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -338,7 +373,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -350,6 +385,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -359,7 +395,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -372,6 +408,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -381,7 +418,7 @@ mod tests { #[test] fn ray_mesh_intersection_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -394,6 +431,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -403,7 +441,7 @@ mod tests { #[test] fn ray_mesh_intersection_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = None; @@ -415,6 +453,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -424,7 +463,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -436,6 +475,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -445,7 +485,7 @@ mod tests { #[test] fn ray_mesh_intersection_not_enough_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0]); @@ -457,6 +497,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -466,7 +507,7 @@ mod tests { #[test] fn ray_mesh_intersection_bad_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 3]); @@ -478,6 +519,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index c1f465b96a..e42dc160e2 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -233,7 +233,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { if let Some(distance) = ray_aabb_intersection_3d( ray, &Aabb3d::new(aabb.center, aabb.half_extents), - &transform.compute_matrix(), + &transform.to_matrix(), ) { aabb_hits_tx.send((FloatOrd(distance), entity)).ok(); } @@ -287,7 +287,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { // Perform the actual ray cast. let _ray_cast_guard = ray_cast_guard.enter(); - let transform = transform.compute_matrix(); + let transform = transform.to_matrix(); let intersection = ray_intersection_over_mesh(mesh, &transform, ray, backfaces); if let Some(intersection) = intersection { diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index faba90cbb9..0406cb61f5 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -269,7 +269,7 @@ pub enum PointerAction { } /// An input event effecting a pointer. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Clone)] pub struct PointerInput { /// The id of the pointer. diff --git a/crates/bevy_platform/Cargo.toml b/crates/bevy_platform/Cargo.toml index 7a4313af39..614ff15fce 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_platform" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides common platform agnostic APIs, as well as platform-specific features for Bevy Engine" homepage = "https://bevy.org" diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index b6e72e24f0..07c6eeae68 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ptr" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Utilities for working with untyped pointers in a more safe way" homepage = "https://bevy.org" diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 704d60d675..15a193d737 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -27,7 +27,9 @@ pub struct Unaligned; /// Trait that is only implemented for [`Aligned`] and [`Unaligned`] to work around the lack of ability /// to have const generics of an enum. pub trait IsAligned: sealed::Sealed {} + impl IsAligned for Aligned {} + impl IsAligned for Unaligned {} mod sealed { diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 8827fc695b..ae3a3a856e 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_reflect" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Dynamically interact with rust types" homepage = "https://bevy.org" @@ -74,10 +74,10 @@ web = ["bevy_platform/web", "uuid?/js"] [dependencies] # bevy -bevy_reflect_derive = { path = "derive", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect_derive = { path = "derive", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", "serialize", ] } @@ -93,7 +93,7 @@ erased-serde = { version = "0.4", default-features = false, features = [ disqualified = { version = "1.0", default-features = false } downcast-rs = { version = "2", default-features = false } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } serde = { version = "1", default-features = false, features = ["alloc"] } assert_type_match = "0.1.1" smallvec = { version = "1.11", default-features = false, optional = true } @@ -109,12 +109,12 @@ uuid = { version = "1.13.1", default-features = false, optional = true, features "serde", ] } variadics_please = "1.1" -wgpu-types = { version = "24", features = [ +wgpu-types = { version = "25", features = [ "serde", ], optional = true, default-features = false } [dev-dependencies] -ron = "0.8.0" +ron = "0.10" rmp-serde = "1.1" bincode = { version = "2.0", features = ["serde"] } serde_json = "1.0.140" diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index a3685941cc..032046ae2f 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_reflect_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_reflect" homepage = "https://bevy.org" @@ -19,7 +19,7 @@ documentation = [] functions = [] [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 55f62b34c8..df9580b820 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -167,6 +167,7 @@ pub struct DynamicArray { } impl DynamicArray { + /// Creates a new [`DynamicArray`]. #[inline] pub fn new(values: Box<[Box]>) -> Self { Self { diff --git a/crates/bevy_reflect/src/attributes.rs b/crates/bevy_reflect/src/attributes.rs index 728102c4b0..4d8154fad3 100644 --- a/crates/bevy_reflect/src/attributes.rs +++ b/crates/bevy_reflect/src/attributes.rs @@ -1,3 +1,5 @@ +//! Types and functions for creating, manipulating and querying [`CustomAttributes`]. + use crate::Reflect; use alloc::boxed::Box; use bevy_utils::TypeIdMap; @@ -98,16 +100,19 @@ struct CustomAttribute { } impl CustomAttribute { + /// Creates a new [`CustomAttribute`] containing `value`. pub fn new(value: T) -> Self { Self { value: Box::new(value), } } + /// Returns a reference to the attribute's value if it is of type `T`, or [`None`] if not. pub fn value(&self) -> Option<&T> { self.value.downcast_ref() } + /// Returns a reference to the attribute's value as a [`Reflect`] trait object. pub fn reflect_value(&self) -> &dyn Reflect { &*self.value } diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index 3f0b275519..2835306b22 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -13,9 +13,12 @@ use derive_more::derive::From; /// A dynamic representation of an enum variant. #[derive(Debug, Default, From)] pub enum DynamicVariant { + /// A unit variant. #[default] Unit, + /// A tuple variant. Tuple(DynamicTuple), + /// A struct variant. Struct(DynamicStruct), } diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index 126c407f23..32e4b96124 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -263,6 +263,7 @@ pub struct VariantFieldIter<'a> { } impl<'a> VariantFieldIter<'a> { + /// Creates a new [`VariantFieldIter`]. pub fn new(container: &'a dyn Enum) -> Self { Self { container, @@ -295,12 +296,16 @@ impl<'a> Iterator for VariantFieldIter<'a> { impl<'a> ExactSizeIterator for VariantFieldIter<'a> {} +/// A field in the current enum variant. pub enum VariantField<'a> { + /// The name and value of a field in a struct variant. Struct(&'a str, &'a dyn PartialReflect), + /// The value of a field in a tuple variant. Tuple(&'a dyn PartialReflect), } impl<'a> VariantField<'a> { + /// Returns the name of a struct variant field, or [`None`] for a tuple variant field. pub fn name(&self) -> Option<&'a str> { if let Self::Struct(name, ..) = self { Some(*name) @@ -309,6 +314,7 @@ impl<'a> VariantField<'a> { } } + /// Gets a reference to the value of this field. pub fn value(&self) -> &'a dyn PartialReflect { match *self { Self::Struct(_, value) | Self::Tuple(value) => value, diff --git a/crates/bevy_reflect/src/enums/variants.rs b/crates/bevy_reflect/src/enums/variants.rs index 55ccb8efb1..d4fcc2845a 100644 --- a/crates/bevy_reflect/src/enums/variants.rs +++ b/crates/bevy_reflect/src/enums/variants.rs @@ -47,7 +47,9 @@ pub enum VariantInfoError { /// [type]: VariantType #[error("variant type mismatch: expected {expected:?}, received {received:?}")] TypeMismatch { + /// Expected variant type. expected: VariantType, + /// Received variant type. received: VariantType, }, } @@ -84,6 +86,7 @@ pub enum VariantInfo { } impl VariantInfo { + /// The name of the enum variant. pub fn name(&self) -> &'static str { match self { Self::Struct(info) => info.name(), diff --git a/crates/bevy_reflect/src/error.rs b/crates/bevy_reflect/src/error.rs index a13b55cdc0..d8bb8a9e14 100644 --- a/crates/bevy_reflect/src/error.rs +++ b/crates/bevy_reflect/src/error.rs @@ -11,14 +11,20 @@ pub enum ReflectCloneError { /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`PartialReflect::reflect_clone` not implemented for `{type_path}`")] - NotImplemented { type_path: Cow<'static, str> }, + NotImplemented { + /// The fully qualified path of the type that [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone) is not implemented for. + type_path: Cow<'static, str>, + }, /// The type cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// This type should be returned when a type is intentionally opting out of reflection cloning. /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`{type_path}` cannot be made cloneable for `PartialReflect::reflect_clone`")] - NotCloneable { type_path: Cow<'static, str> }, + NotCloneable { + /// The fully qualified path of the type that cannot be cloned via [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone). + type_path: Cow<'static, str>, + }, /// The field cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// When [deriving `Reflect`], this usually means that a field marked with `#[reflect(ignore)]` @@ -33,8 +39,11 @@ pub enum ReflectCloneError { full_path(.field, .variant.as_deref(), .container_type_path) )] FieldNotCloneable { + /// Struct field or enum variant field which cannot be cloned. field: FieldId, + /// Variant this field is part of if the container is an enum, otherwise [`None`]. variant: Option>, + /// Fully qualified path of the type containing this field. container_type_path: Cow<'static, str>, }, /// Could not downcast to the expected type. @@ -44,7 +53,9 @@ pub enum ReflectCloneError { /// [`Reflect`]: crate::Reflect #[error("expected downcast to `{expected}`, but received `{received}`")] FailedDowncast { + /// The fully qualified path of the type that was expected. expected: Cow<'static, str>, + /// The fully qualified path of the type that was received. received: Cow<'static, str>, }, } diff --git a/crates/bevy_reflect/src/fields.rs b/crates/bevy_reflect/src/fields.rs index 21d4ccd98a..53223835b3 100644 --- a/crates/bevy_reflect/src/fields.rs +++ b/crates/bevy_reflect/src/fields.rs @@ -82,6 +82,7 @@ pub struct UnnamedField { } impl UnnamedField { + /// Create a new [`UnnamedField`]. pub fn new(index: usize) -> Self { Self { index, @@ -135,7 +136,9 @@ impl UnnamedField { /// A representation of a field's accessor. #[derive(Clone, Debug, PartialEq, Eq)] pub enum FieldId { + /// Access a field by name. Named(Cow<'static, str>), + /// Access a field by index. Unnamed(usize), } diff --git a/crates/bevy_reflect/src/func/args/arg.rs b/crates/bevy_reflect/src/func/args/arg.rs index 8ca03aafd3..1c157a6b2f 100644 --- a/crates/bevy_reflect/src/func/args/arg.rs +++ b/crates/bevy_reflect/src/func/args/arg.rs @@ -196,8 +196,11 @@ impl<'a> Arg<'a> { /// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug)] pub enum ArgValue<'a> { + /// An owned argument. Owned(Box), + /// An immutable reference argument. Ref(&'a dyn PartialReflect), + /// A mutable reference argument. Mut(&'a mut dyn PartialReflect), } diff --git a/crates/bevy_reflect/src/func/args/error.rs b/crates/bevy_reflect/src/func/args/error.rs index bd32bd5e5a..20b6cd6220 100644 --- a/crates/bevy_reflect/src/func/args/error.rs +++ b/crates/bevy_reflect/src/func/args/error.rs @@ -12,15 +12,21 @@ pub enum ArgError { /// The argument is not the expected type. #[error("expected `{expected}` but received `{received}` (@ argument index {index})")] UnexpectedType { + /// Argument index. index: usize, + /// Expected argument type path. expected: Cow<'static, str>, + /// Received argument type path. received: Cow<'static, str>, }, /// The argument has the wrong ownership. #[error("expected {expected} value but received {received} value (@ argument index {index})")] InvalidOwnership { + /// Argument index. index: usize, + /// Expected ownership. expected: Ownership, + /// Received ownership. received: Ownership, }, /// Occurs when attempting to access an argument from an empty [`ArgList`]. diff --git a/crates/bevy_reflect/src/func/dynamic_function.rs b/crates/bevy_reflect/src/func/dynamic_function.rs index 054e8ffaff..ab1d70e4ed 100644 --- a/crates/bevy_reflect/src/func/dynamic_function.rs +++ b/crates/bevy_reflect/src/func/dynamic_function.rs @@ -439,6 +439,7 @@ impl PartialReflect for DynamicFunction<'static> { } impl MaybeTyped for DynamicFunction<'static> {} + impl RegisterForReflection for DynamicFunction<'static> {} impl_type_path!((in bevy_reflect) DynamicFunction<'env>); diff --git a/crates/bevy_reflect/src/func/error.rs b/crates/bevy_reflect/src/func/error.rs index d9d105db1b..dc442e9da8 100644 --- a/crates/bevy_reflect/src/func/error.rs +++ b/crates/bevy_reflect/src/func/error.rs @@ -18,11 +18,18 @@ pub enum FunctionError { ArgError(#[from] ArgError), /// The number of arguments provided does not match the expected number. #[error("received {received} arguments but expected one of {expected:?}")] - ArgCountMismatch { expected: ArgCount, received: usize }, + ArgCountMismatch { + /// Expected argument count. [`ArgCount`] for overloaded functions will contain multiple possible counts. + expected: ArgCount, + /// Number of arguments received. + received: usize, + }, /// No overload was found for the given set of arguments. #[error("no overload found for arguments with signature `{received:?}`, expected one of `{expected:?}`")] NoOverload { + /// The set of available argument signatures. expected: HashSet, + /// The received argument signature. received: ArgumentSignature, }, } @@ -47,6 +54,9 @@ pub enum FunctionOverloadError { /// An error that occurs when attempting to add a function overload with a duplicate signature. #[error("could not add function overload: duplicate found for signature `{0:?}`")] DuplicateSignature(ArgumentSignature), + /// An attempt was made to add an overload with more than [`ArgCount::MAX_COUNT`] arguments. + /// + /// [`ArgCount::MAX_COUNT`]: crate::func::args::ArgCount #[error( "argument signature `{:?}` has too many arguments (max {})", 0, diff --git a/crates/bevy_reflect/src/func/info.rs b/crates/bevy_reflect/src/func/info.rs index 4b130e5772..2f5f82fbf5 100644 --- a/crates/bevy_reflect/src/func/info.rs +++ b/crates/bevy_reflect/src/func/info.rs @@ -235,6 +235,12 @@ impl TryFrom<[SignatureInfo; N]> for FunctionInfo { } } +/// Type information for the signature of a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// +/// Every [`FunctionInfo`] contains one or more [`SignatureInfo`]s. +/// +/// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct SignatureInfo { name: Option>, diff --git a/crates/bevy_reflect/src/func/mod.rs b/crates/bevy_reflect/src/func/mod.rs index 74a89282c6..237bc9eafc 100644 --- a/crates/bevy_reflect/src/func/mod.rs +++ b/crates/bevy_reflect/src/func/mod.rs @@ -42,7 +42,7 @@ //! //! A "function" is a callable that does not capture its environment. //! These are typically defined with the `fn` keyword, which are referred to as _named_ functions. -//! But they are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. +//! But there are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. //! //! ```rust //! // This is a named function: diff --git a/crates/bevy_reflect/src/func/registry.rs b/crates/bevy_reflect/src/func/registry.rs index e476353b8b..08ed7bd7f1 100644 --- a/crates/bevy_reflect/src/func/registry.rs +++ b/crates/bevy_reflect/src/func/registry.rs @@ -336,6 +336,7 @@ impl Debug for FunctionRegistry { /// A synchronized wrapper around a [`FunctionRegistry`]. #[derive(Clone, Default, Debug)] pub struct FunctionRegistryArc { + /// The wrapped [`FunctionRegistry`]. pub internal: Arc>, } diff --git a/crates/bevy_reflect/src/func/signature.rs b/crates/bevy_reflect/src/func/signature.rs index 9102049eee..cedeaca952 100644 --- a/crates/bevy_reflect/src/func/signature.rs +++ b/crates/bevy_reflect/src/func/signature.rs @@ -90,6 +90,7 @@ impl<'a, 'b> ArgListSignature<'a, 'b> { } impl Eq for ArgListSignature<'_, '_> {} + impl PartialEq for ArgListSignature<'_, '_> { fn eq(&self, other: &Self) -> bool { self.len() == other.len() && self.iter().eq(other.iter()) diff --git a/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs b/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs index e579ace206..df68b425f6 100644 --- a/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs +++ b/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs @@ -2,7 +2,7 @@ use crate::{ error::ReflectCloneError, generics::{Generics, TypeParamInfo}, kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, - map::{map_apply, map_partial_eq, map_try_apply, Map, MapInfo, MapIter}, + map::{map_apply, map_partial_eq, map_try_apply, Map, MapInfo}, prelude::*, reflect::{impl_full_reflect, ApplyError}, type_info::{MaybeTyped, TypeInfo, Typed}, @@ -31,27 +31,15 @@ where .map(|value| value as &mut dyn PartialReflect) } - fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { - self.iter() - .nth(index) - .map(|(key, value)| (key as &dyn PartialReflect, value as &dyn PartialReflect)) - } - - fn get_at_mut( - &mut self, - index: usize, - ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { - self.iter_mut() - .nth(index) - .map(|(key, value)| (key as &dyn PartialReflect, value as &mut dyn PartialReflect)) - } - fn len(&self) -> usize { Self::len(self) } - fn iter(&self) -> MapIter { - MapIter::new(self) + fn iter(&self) -> Box + '_> { + Box::new( + self.iter() + .map(|(k, v)| (k as &dyn PartialReflect, v as &dyn PartialReflect)), + ) } fn drain(&mut self) -> Vec<(Box, Box)> { @@ -68,6 +56,10 @@ where result } + fn retain(&mut self, f: &mut dyn FnMut(&dyn PartialReflect, &mut dyn PartialReflect) -> bool) { + self.retain(move |k, v| f(k, v)); + } + fn insert_boxed( &mut self, key: Box, diff --git a/crates/bevy_reflect/src/impls/core/primitives.rs b/crates/bevy_reflect/src/impls/core/primitives.rs index 8bee33b4c8..3600f2ece5 100644 --- a/crates/bevy_reflect/src/impls/core/primitives.rs +++ b/crates/bevy_reflect/src/impls/core/primitives.rs @@ -282,6 +282,7 @@ impl GetTypeRegistration for &'static str { let mut registration = TypeRegistration::of::(); registration.insert::(FromType::::from_type()); registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); registration } } diff --git a/crates/bevy_reflect/src/impls/macros/map.rs b/crates/bevy_reflect/src/impls/macros/map.rs index ce621b7f78..d356ddba58 100644 --- a/crates/bevy_reflect/src/impls/macros/map.rs +++ b/crates/bevy_reflect/src/impls/macros/map.rs @@ -19,27 +19,12 @@ macro_rules! impl_reflect_for_hashmap { .map(|value| value as &mut dyn $crate::reflect::PartialReflect) } - fn get_at(&self, index: usize) -> Option<(&dyn $crate::reflect::PartialReflect, &dyn $crate::reflect::PartialReflect)> { - self.iter() - .nth(index) - .map(|(key, value)| (key as &dyn $crate::reflect::PartialReflect, value as &dyn $crate::reflect::PartialReflect)) - } - - fn get_at_mut( - &mut self, - index: usize, - ) -> Option<(&dyn $crate::reflect::PartialReflect, &mut dyn $crate::reflect::PartialReflect)> { - self.iter_mut().nth(index).map(|(key, value)| { - (key as &dyn $crate::reflect::PartialReflect, value as &mut dyn $crate::reflect::PartialReflect) - }) - } - fn len(&self) -> usize { Self::len(self) } - fn iter(&self) -> $crate::map::MapIter { - $crate::map::MapIter::new(self) + fn iter(&self) -> bevy_platform::prelude::Box + '_> { + bevy_platform::prelude::Box::new(self.iter().map(|(k, v)| (k as &dyn $crate::reflect::PartialReflect, v as &dyn $crate::reflect::PartialReflect))) } fn drain(&mut self) -> bevy_platform::prelude::Vec<(bevy_platform::prelude::Box, bevy_platform::prelude::Box)> { @@ -53,6 +38,10 @@ macro_rules! impl_reflect_for_hashmap { .collect() } + fn retain(&mut self, f: &mut dyn FnMut(&dyn $crate::reflect::PartialReflect, &mut dyn $crate::reflect::PartialReflect) -> bool) { + self.retain(move |key, value| f(key, value)); + } + fn to_dynamic_map(&self) -> $crate::map::DynamicMap { let mut dynamic_map = $crate::map::DynamicMap::default(); dynamic_map.set_represented_type($crate::reflect::PartialReflect::get_represented_type_info(self)); diff --git a/crates/bevy_reflect/src/impls/macros/set.rs b/crates/bevy_reflect/src/impls/macros/set.rs index e00e764e17..599ec1c0c8 100644 --- a/crates/bevy_reflect/src/impls/macros/set.rs +++ b/crates/bevy_reflect/src/impls/macros/set.rs @@ -28,6 +28,10 @@ macro_rules! impl_reflect_for_hashset { .collect() } + fn retain(&mut self, f: &mut dyn FnMut(&dyn $crate::reflect::PartialReflect) -> bool) { + self.retain(move |value| f(value)); + } + fn insert_boxed(&mut self, value: bevy_platform::prelude::Box) -> bool { let value = V::take_from_reflect(value).unwrap_or_else(|value| { panic!( diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index 942bcbe83f..561111a901 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -77,6 +77,7 @@ where .collect() } } + impl PartialReflect for SmallVec where T::Item: FromReflect + MaybeTyped + TypePath, diff --git a/crates/bevy_reflect/src/kind.rs b/crates/bevy_reflect/src/kind.rs index 3eef10d0e5..8988b30aa5 100644 --- a/crates/bevy_reflect/src/kind.rs +++ b/crates/bevy_reflect/src/kind.rs @@ -134,7 +134,9 @@ macro_rules! impl_reflect_kind_conversions { #[derive(Debug, Error)] #[error("kind mismatch: expected {expected:?}, received {received:?}")] pub struct ReflectKindMismatchError { + /// Expected kind. pub expected: ReflectKind, + /// Received kind. pub received: ReflectKind, } @@ -176,18 +178,49 @@ macro_rules! impl_cast_method { /// /// ["kinds"]: ReflectKind pub enum ReflectRef<'a> { + /// An immutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a dyn Struct), + /// An immutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a dyn TupleStruct), + /// An immutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a dyn Tuple), + /// An immutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a dyn List), + /// An immutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a dyn Array), + /// An immutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a dyn Map), + /// An immutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a dyn Set), + /// An immutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a dyn Enum), + /// An immutable reference to a [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(&'a dyn Function), + /// An immutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a dyn PartialReflect), } + impl_reflect_kind_conversions!(ReflectRef<'_>); impl<'a> ReflectRef<'a> { @@ -211,18 +244,49 @@ impl<'a> ReflectRef<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectMut<'a> { + /// A mutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a mut dyn Struct), + /// A mutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a mut dyn TupleStruct), + /// A mutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a mut dyn Tuple), + /// A mutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a mut dyn List), + /// A mutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a mut dyn Array), + /// A mutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a mut dyn Map), + /// A mutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a mut dyn Set), + /// A mutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a mut dyn Enum), #[cfg(feature = "functions")] + /// A mutable reference to a [function-like] type. + /// + /// [function-like]: Function Function(&'a mut dyn Function), + /// A mutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a mut dyn PartialReflect), } + impl_reflect_kind_conversions!(ReflectMut<'_>); impl<'a> ReflectMut<'a> { @@ -246,18 +310,49 @@ impl<'a> ReflectMut<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectOwned { + /// An owned [struct-like] type. + /// + /// [struct-like]: Struct Struct(Box), + /// An owned [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(Box), + /// An owned [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(Box), + /// An owned [list-like] type. + /// + /// [list-like]: List List(Box), + /// An owned [array-like] type. + /// + /// [array-like]: Array Array(Box), + /// An owned [map-like] type. + /// + /// [map-like]: Map Map(Box), + /// An owned [set-like] type. + /// + /// [set-like]: Set Set(Box), + /// An owned [enum-like] type. + /// + /// [enum-like]: Enum Enum(Box), + /// An owned [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(Box), + /// An owned [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(Box), } + impl_reflect_kind_conversions!(ReflectOwned); impl ReflectOwned { diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 0f399afd59..eaf601ef0d 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1,4 +1,3 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr( any(docsrs, docsrs_dep), expect( @@ -1002,7 +1001,7 @@ mod tests { /// If we don't append the strings in the `TypePath` derive correctly (i.e. explicitly specifying the type), /// we'll get a compilation error saying that "`&String` cannot be added to `String`". /// - /// So this test just ensures that we do do that correctly. + /// So this test just ensures that we do that correctly. /// /// This problem is a known issue and is unexpectedly expected behavior: /// - @@ -1584,7 +1583,6 @@ mod tests { foo.apply(&foo_patch); let mut hash_map = >::default(); - hash_map.insert(1, 1); hash_map.insert(2, 3); hash_map.insert(3, 4); diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 1a1fcefb63..e717869202 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -56,15 +56,6 @@ pub trait Map: PartialReflect { /// If no value is associated with `key`, returns `None`. fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect>; - /// Returns the key-value pair at `index` by reference, or `None` if out of bounds. - fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)>; - - /// Returns the key-value pair at `index` by reference where the value is a mutable reference, or `None` if out of bounds. - fn get_at_mut( - &mut self, - index: usize, - ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)>; - /// Returns the number of elements in the map. fn len(&self) -> usize; @@ -74,13 +65,18 @@ pub trait Map: PartialReflect { } /// Returns an iterator over the key-value pairs of the map. - fn iter(&self) -> MapIter; + fn iter(&self) -> Box + '_>; /// Drain the key-value pairs of this map to get a vector of owned values. /// /// After calling this function, `self` will be empty. fn drain(&mut self) -> Vec<(Box, Box)>; + /// Retain only the elements specified by the predicate. + /// + /// In other words, remove all pairs `(k, v)` such that `f(&k, &mut v)` returns `false`. + fn retain(&mut self, f: &mut dyn FnMut(&dyn PartialReflect, &mut dyn PartialReflect) -> bool); + /// Creates a new [`DynamicMap`] from this map. fn to_dynamic_map(&self) -> DynamicMap { let mut map = DynamicMap::default(); @@ -192,6 +188,8 @@ impl MapInfo { impl_generic_info_methods!(generics); } +/// Used to produce an error message when an attempt is made to hash +/// a [`PartialReflect`] value that does not support hashing. #[macro_export] macro_rules! hash_error { ( $key:expr ) => {{ @@ -216,12 +214,11 @@ macro_rules! hash_error { }} } -/// An ordered mapping between reflected values. +/// An unordered mapping between reflected values. #[derive(Default)] pub struct DynamicMap { represented_type: Option<&'static TypeInfo>, - values: Vec<(Box, Box)>, - indices: HashTable, + hash_table: HashTable<(Box, Box)>, } impl DynamicMap { @@ -252,13 +249,12 @@ impl DynamicMap { value.reflect_hash().expect(&hash_error!(value)) } - fn internal_eq<'a>( - value: &'a dyn PartialReflect, - values: &'a [(Box, Box)], - ) -> impl FnMut(&usize) -> bool + 'a { - |&index| { - value - .reflect_partial_eq(&*values[index].0) + fn internal_eq( + key: &dyn PartialReflect, + ) -> impl FnMut(&(Box, Box)) -> bool + '_ { + |(other, _)| { + key + .reflect_partial_eq(&**other) .expect("underlying type does not reflect `PartialEq` and hence doesn't support equality checks") } } @@ -266,46 +262,33 @@ impl DynamicMap { impl Map for DynamicMap { fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { - let hash = Self::internal_hash(key); - let eq = Self::internal_eq(key, &self.values); - self.indices - .find(hash, eq) - .map(|&index| &*self.values[index].1) + self.hash_table + .find(Self::internal_hash(key), Self::internal_eq(key)) + .map(|(_, value)| &**value) } fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { - let hash = Self::internal_hash(key); - let eq = Self::internal_eq(key, &self.values); - self.indices - .find(hash, eq) - .map(|&index| &mut *self.values[index].1) - } - - fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { - self.values - .get(index) - .map(|(key, value)| (&**key, &**value)) - } - - fn get_at_mut( - &mut self, - index: usize, - ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { - self.values - .get_mut(index) - .map(|(key, value)| (&**key, &mut **value)) + self.hash_table + .find_mut(Self::internal_hash(key), Self::internal_eq(key)) + .map(|(_, value)| &mut **value) } fn len(&self) -> usize { - self.values.len() + self.hash_table.len() } - fn iter(&self) -> MapIter { - MapIter::new(self) + fn iter(&self) -> Box + '_> { + let iter = self.hash_table.iter().map(|(k, v)| (&**k, &**v)); + Box::new(iter) } fn drain(&mut self) -> Vec<(Box, Box)> { - self.values.drain(..).collect() + self.hash_table.drain().collect() + } + + fn retain(&mut self, f: &mut dyn FnMut(&dyn PartialReflect, &mut dyn PartialReflect) -> bool) { + self.hash_table + .retain(move |(key, value)| f(&**key, &mut **value)); } fn insert_boxed( @@ -320,20 +303,15 @@ impl Map for DynamicMap { ); let hash = Self::internal_hash(&*key); - let eq = Self::internal_eq(&*key, &self.values); - match self.indices.find(hash, eq) { - Some(&index) => { - let (key_ref, value_ref) = &mut self.values[index]; - *key_ref = key; - let old_value = core::mem::replace(value_ref, value); - Some(old_value) - } + let eq = Self::internal_eq(&*key); + match self.hash_table.find_mut(hash, eq) { + Some((_, old)) => Some(core::mem::replace(old, value)), None => { - let index = self.values.len(); - self.values.push((key, value)); - self.indices.insert_unique(hash, index, |&index| { - Self::internal_hash(&*self.values[index].0) - }); + self.hash_table.insert_unique( + Self::internal_hash(key.as_ref()), + (key, value), + |(key, _)| Self::internal_hash(&**key), + ); None } } @@ -341,26 +319,10 @@ impl Map for DynamicMap { fn remove(&mut self, key: &dyn PartialReflect) -> Option> { let hash = Self::internal_hash(key); - let eq = Self::internal_eq(key, &self.values); - match self.indices.find_entry(hash, eq) { + let eq = Self::internal_eq(key); + match self.hash_table.find_entry(hash, eq) { Ok(entry) => { - let (index, _) = entry.remove(); - let (_, old_value) = self.values.swap_remove(index); - - // The `swap_remove` might have moved the last element of `values` - // to `index`, so we might need to fix up its index in `indices`. - // If the removed element was also the last element there's nothing to - // fixup and this will return `None`, otherwise it returns the key - // whose index needs to be fixed up. - if let Some((moved_key, _)) = self.values.get(index) { - let hash = Self::internal_hash(&**moved_key); - let moved_index = self - .indices - .find_mut(hash, |&moved_index| moved_index == self.values.len()) - .expect("key inserted in a `DynamicMap` is no longer present, this means its reflected `Hash` might be incorrect"); - *moved_index = index; - } - + let ((_, old_value), _) = entry.remove(); Some(old_value) } Err(_) => None, @@ -449,35 +411,6 @@ impl Debug for DynamicMap { } } -/// An iterator over the key-value pairs of a [`Map`]. -pub struct MapIter<'a> { - map: &'a dyn Map, - index: usize, -} - -impl MapIter<'_> { - /// Creates a new [`MapIter`]. - #[inline] - pub const fn new(map: &dyn Map) -> MapIter { - MapIter { map, index: 0 } - } -} - -impl<'a> Iterator for MapIter<'a> { - type Item = (&'a dyn PartialReflect, &'a dyn PartialReflect); - - fn next(&mut self) -> Option { - let value = self.map.get_at(self.index); - self.index += value.is_some() as usize; - value - } - - fn size_hint(&self) -> (usize, Option) { - let size = self.map.len(); - (size, Some(size)) - } -} - impl FromIterator<(Box, Box)> for DynamicMap { fn from_iter, Box)>>( items: I, @@ -502,24 +435,30 @@ impl FromIterator<(K, V)> for DynamicMap { impl IntoIterator for DynamicMap { type Item = (Box, Box); - type IntoIter = alloc::vec::IntoIter; + type IntoIter = bevy_platform::collections::hash_table::IntoIter; fn into_iter(self) -> Self::IntoIter { - self.values.into_iter() + self.hash_table.into_iter() } } impl<'a> IntoIterator for &'a DynamicMap { type Item = (&'a dyn PartialReflect, &'a dyn PartialReflect); - type IntoIter = MapIter<'a>; + type IntoIter = core::iter::Map< + bevy_platform::collections::hash_table::Iter< + 'a, + (Box, Box), + >, + fn(&'a (Box, Box)) -> Self::Item, + >; fn into_iter(self) -> Self::IntoIter { - self.iter() + self.hash_table + .iter() + .map(|(k, v)| (k.as_ref(), v.as_ref())) } } -impl<'a> ExactSizeIterator for MapIter<'a> {} - /// Compares a [`Map`] with a [`PartialReflect`] value. /// /// Returns true if and only if all of the following are true: @@ -582,6 +521,7 @@ pub fn map_debug(dyn_map: &dyn Map, f: &mut Formatter<'_>) -> core::fmt::Result /// Applies the elements of reflected map `b` to the corresponding elements of map `a`. /// /// If a key from `b` does not exist in `a`, the value is cloned and inserted. +/// If a key from `a` does not exist in `b`, the value is removed. /// /// # Panics /// @@ -597,6 +537,7 @@ pub fn map_apply(a: &mut M, b: &dyn PartialReflect) { /// and returns a Result. /// /// If a key from `b` does not exist in `a`, the value is cloned and inserted. +/// If a key from `a` does not exist in `b`, the value is removed. /// /// # Errors /// @@ -613,117 +554,17 @@ pub fn map_try_apply(a: &mut M, b: &dyn PartialReflect) -> Result<(), Ap a.insert_boxed(key.to_dynamic(), b_value.to_dynamic()); } } + a.retain(&mut |key, _| map_value.get(key).is_some()); Ok(()) } #[cfg(test)] mod tests { + + use crate::PartialReflect; + use super::{DynamicMap, Map}; - use alloc::{ - borrow::ToOwned, - string::{String, ToString}, - }; - - #[test] - fn test_into_iter() { - let expected = ["foo", "bar", "baz"]; - - let mut map = DynamicMap::default(); - map.insert(0usize, expected[0].to_string()); - map.insert(1usize, expected[1].to_string()); - map.insert(2usize, expected[2].to_string()); - - for (index, item) in map.into_iter().enumerate() { - let key = item - .0 - .try_take::() - .expect("couldn't downcast to usize"); - let value = item - .1 - .try_take::() - .expect("couldn't downcast to String"); - assert_eq!(index, key); - assert_eq!(expected[index], value); - } - } - - #[test] - fn test_map_get_at() { - let values = ["first", "second", "third"]; - let mut map = DynamicMap::default(); - map.insert(0usize, values[0].to_string()); - map.insert(1usize, values[1].to_string()); - map.insert(1usize, values[2].to_string()); - - let (key_r, value_r) = map.get_at(1).expect("Item wasn't found"); - let value = value_r - .try_downcast_ref::() - .expect("Couldn't downcast to String"); - let key = key_r - .try_downcast_ref::() - .expect("Couldn't downcast to usize"); - assert_eq!(key, &1usize); - assert_eq!(value, &values[2].to_owned()); - - assert!(map.get_at(2).is_none()); - map.remove(&1usize); - assert!(map.get_at(1).is_none()); - } - - #[test] - fn test_map_get_at_mut() { - let values = ["first", "second", "third"]; - let mut map = DynamicMap::default(); - map.insert(0usize, values[0].to_string()); - map.insert(1usize, values[1].to_string()); - map.insert(1usize, values[2].to_string()); - - let (key_r, value_r) = map.get_at_mut(1).expect("Item wasn't found"); - let value = value_r - .try_downcast_mut::() - .expect("Couldn't downcast to String"); - let key = key_r - .try_downcast_ref::() - .expect("Couldn't downcast to usize"); - assert_eq!(key, &1usize); - assert_eq!(value, &mut values[2].to_owned()); - - value.clone_from(&values[0].to_owned()); - - assert_eq!( - map.get(&1usize) - .expect("Item wasn't found") - .try_downcast_ref::() - .expect("Couldn't downcast to String"), - &values[0].to_owned() - ); - - assert!(map.get_at(2).is_none()); - } - - #[test] - fn next_index_increment() { - let values = ["first", "last"]; - let mut map = DynamicMap::default(); - map.insert(0usize, values[0]); - map.insert(1usize, values[1]); - - let mut iter = map.iter(); - let size = iter.len(); - - for _ in 0..2 { - let prev_index = iter.index; - assert!(iter.next().is_some()); - assert_eq!(prev_index, iter.index - 1); - } - - // When None we should no longer increase index - for _ in 0..2 { - assert!(iter.next().is_none()); - assert_eq!(size, iter.index); - } - } #[test] fn remove() { @@ -741,4 +582,21 @@ mod tests { assert!(map.remove(&1).is_none()); assert!(map.get(&1).is_none()); } + + #[test] + fn apply() { + let mut map_a = DynamicMap::default(); + map_a.insert(0, 0); + map_a.insert(1, 1); + + let mut map_b = DynamicMap::default(); + map_b.insert(10, 10); + map_b.insert(1, 5); + + map_a.apply(&map_b); + + assert!(map_a.get(&0).is_none()); + assert_eq!(map_a.get(&1).unwrap().try_downcast_ref(), Some(&5)); + assert_eq!(map_a.get(&10).unwrap().try_downcast_ref(), Some(&10)); + } } diff --git a/crates/bevy_reflect/src/path/error.rs b/crates/bevy_reflect/src/path/error.rs index 0e900c8315..00188a4cc3 100644 --- a/crates/bevy_reflect/src/path/error.rs +++ b/crates/bevy_reflect/src/path/error.rs @@ -74,6 +74,7 @@ impl<'a> AccessError<'a> { self.offset.as_ref() } } + impl fmt::Display for AccessError<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let AccessError { @@ -126,4 +127,5 @@ impl fmt::Display for AccessError<'_> { } } } + impl core::error::Error for AccessError<'_> {} diff --git a/crates/bevy_reflect/src/path/mod.rs b/crates/bevy_reflect/src/path/mod.rs index a52bbb6aaa..f0434686ee 100644 --- a/crates/bevy_reflect/src/path/mod.rs +++ b/crates/bevy_reflect/src/path/mod.rs @@ -82,6 +82,7 @@ pub trait ReflectPath<'a>: Sized { }) } } + impl<'a> ReflectPath<'a> for &'a str { fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for (access, offset) in PathParser::new(self) { @@ -437,6 +438,7 @@ impl ParsedPath { Ok(Self(parts)) } } + impl<'a> ReflectPath<'a> for &'a ParsedPath { fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for OffsetAccess { access, offset } in &self.0 { @@ -454,11 +456,13 @@ impl<'a> ReflectPath<'a> for &'a ParsedPath { Ok(root) } } + impl From<[OffsetAccess; N]> for ParsedPath { fn from(value: [OffsetAccess; N]) -> Self { ParsedPath(value.to_vec()) } } + impl From>> for ParsedPath { fn from(value: Vec>) -> Self { ParsedPath( @@ -472,6 +476,7 @@ impl From>> for ParsedPath { ) } } + impl From<[Access<'static>; N]> for ParsedPath { fn from(value: [Access<'static>; N]) -> Self { value.to_vec().into() @@ -493,12 +498,14 @@ impl fmt::Display for ParsedPath { Ok(()) } } + impl core::ops::Index for ParsedPath { type Output = OffsetAccess; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } + impl core::ops::IndexMut for ParsedPath { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] diff --git a/crates/bevy_reflect/src/path/parse.rs b/crates/bevy_reflect/src/path/parse.rs index 2ab2939a30..be5856834a 100644 --- a/crates/bevy_reflect/src/path/parse.rs +++ b/crates/bevy_reflect/src/path/parse.rs @@ -38,6 +38,7 @@ pub(super) struct PathParser<'a> { path: &'a str, remaining: &'a [u8], } + impl<'a> PathParser<'a> { pub(super) fn new(path: &'a str) -> Self { let remaining = path.as_bytes(); @@ -103,6 +104,7 @@ impl<'a> PathParser<'a> { self.path.len() - self.remaining.len() } } + impl<'a> Iterator for PathParser<'a> { type Item = (Result, ReflectPathError<'a>>, usize); @@ -149,6 +151,7 @@ enum Token<'a> { CloseBracket = b']', Ident(Ident<'a>), } + impl fmt::Display for Token<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -160,6 +163,7 @@ impl fmt::Display for Token<'_> { } } } + impl<'a> Token<'a> { const SYMBOLS: &'static [u8] = b".#[]"; fn symbol_from_byte(byte: u8) -> Option { diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 04e4a2a4b0..1bd1795066 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -21,32 +21,47 @@ pub enum ApplyError { #[error("attempted to apply `{from_kind}` to `{to_kind}`")] /// Attempted to apply the wrong [kind](ReflectKind) to a type, e.g. a struct to an enum. MismatchedKinds { + /// Kind of the value we attempted to apply. from_kind: ReflectKind, + /// Kind of the type we attempted to apply the value to. to_kind: ReflectKind, }, #[error("enum variant `{variant_name}` doesn't have a field named `{field_name}`")] /// Enum variant that we tried to apply to was missing a field. MissingEnumField { + /// Name of the enum variant. variant_name: Box, + /// Name of the missing field. field_name: Box, }, #[error("`{from_type}` is not `{to_type}`")] /// Tried to apply incompatible types. MismatchedTypes { + /// Type of the value we attempted to apply. from_type: Box, + /// Type we attempted to apply the value to. to_type: Box, }, #[error("attempted to apply type with {from_size} size to a type with {to_size} size")] - /// Attempted to apply to types with mismatched sizes, e.g. a [u8; 4] to [u8; 3]. - DifferentSize { from_size: usize, to_size: usize }, + /// Attempted to apply an [array-like] type to another of different size, e.g. a [u8; 4] to [u8; 3]. + /// + /// [array-like]: crate::Array + DifferentSize { + /// Size of the value we attempted to apply, in elements. + from_size: usize, + /// Size of the type we attempted to apply the value to, in elements. + to_size: usize, + }, #[error("variant with name `{variant_name}` does not exist on enum `{enum_name}`")] /// The enum we tried to apply to didn't contain a variant with the give name. UnknownVariant { + /// Name of the enum. enum_name: Box, + /// Name of the missing variant. variant_name: Box, }, } @@ -152,10 +167,10 @@ where /// and excess elements in `value` are appended to `self`. /// - If `Self` is a [`Map`], then for each key in `value`, the associated /// value is applied to the value associated with the same key in `self`. - /// Keys which are not present in `self` are inserted. + /// Keys which are not present in `self` are inserted, and keys from `self` which are not present in `value` are removed. /// - If `Self` is a [`Set`], then each element of `value` is applied to the corresponding /// element of `Self`. If an element of `value` does not exist in `Self` then it is - /// cloned and inserted. + /// cloned and inserted. If an element from `self` is not present in `value` then it is removed. /// - If `Self` is none of these, then `value` is downcast to `Self`, cloned, and /// assigned to `self`. /// diff --git a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs index f92a8e68e2..8a216f87b9 100644 --- a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs @@ -42,6 +42,9 @@ use serde::Deserializer; /// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer /// [via the registry]: TypeRegistry::register_type_data pub trait DeserializeWithRegistry<'de>: Sized { + /// Deserialize this value using the given [Deserializer] and [`TypeRegistry`]. + /// + /// [`Deserializer`]: ::serde::Deserializer fn deserialize(deserializer: D, registry: &TypeRegistry) -> Result where D: Deserializer<'de>; diff --git a/crates/bevy_reflect/src/serde/de/registrations.rs b/crates/bevy_reflect/src/serde/de/registrations.rs index adc0025c54..768b8ed32f 100644 --- a/crates/bevy_reflect/src/serde/de/registrations.rs +++ b/crates/bevy_reflect/src/serde/de/registrations.rs @@ -15,6 +15,7 @@ pub struct TypeRegistrationDeserializer<'a> { } impl<'a> TypeRegistrationDeserializer<'a> { + /// Creates a new [`TypeRegistrationDeserializer`]. pub fn new(registry: &'a TypeRegistry) -> Self { Self { registry } } diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index 032590e0c7..2ee47d4a7f 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -1,3 +1,5 @@ +//! Serde integration for reflected types. + mod de; mod ser; mod type_data; diff --git a/crates/bevy_reflect/src/serde/ser/processor.rs b/crates/bevy_reflect/src/serde/ser/processor.rs index cf31ab7566..fc35ff883a 100644 --- a/crates/bevy_reflect/src/serde/ser/processor.rs +++ b/crates/bevy_reflect/src/serde/ser/processor.rs @@ -112,15 +112,15 @@ use crate::{PartialReflect, TypeRegistry}; /// } /// } /// -/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result, AssetError> { -/// let mut asset_bytes = Vec::new(); +/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result { +/// let mut asset_string = String::new(); /// /// let processor = HandleProcessor; /// let serializer = ReflectSerializer::with_processor(asset, type_registry, &processor); -/// let mut ron_serializer = ron::Serializer::new(&mut asset_bytes, None)?; +/// let mut ron_serializer = ron::Serializer::new(&mut asset_string, None)?; /// /// serializer.serialize(&mut ron_serializer)?; -/// Ok(asset_bytes) +/// Ok(asset_string) /// } /// ``` /// diff --git a/crates/bevy_reflect/src/serde/ser/serializable.rs b/crates/bevy_reflect/src/serde/ser/serializable.rs index 6a8a4c978f..c83737c842 100644 --- a/crates/bevy_reflect/src/serde/ser/serializable.rs +++ b/crates/bevy_reflect/src/serde/ser/serializable.rs @@ -3,7 +3,9 @@ use core::ops::Deref; /// A type-erased serializable value. pub enum Serializable<'a> { + /// An owned serializable value. Owned(Box), + /// An immutable reference to a serializable value. Borrowed(&'a dyn erased_serde::Serialize), } diff --git a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs index 9c5bfb06f1..f9e6370799 100644 --- a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs @@ -40,6 +40,9 @@ use serde::{Serialize, Serializer}; /// [`ReflectSerializer`]: crate::serde::ReflectSerializer /// [via the registry]: TypeRegistry::register_type_data pub trait SerializeWithRegistry { + /// Serialize this value using the given [Serializer] and [`TypeRegistry`]. + /// + /// [`Serializer`]: ::serde::Serializer fn serialize(&self, serializer: S, registry: &TypeRegistry) -> Result where S: Serializer; diff --git a/crates/bevy_reflect/src/set.rs b/crates/bevy_reflect/src/set.rs index 01888e7825..e464ee4aab 100644 --- a/crates/bevy_reflect/src/set.rs +++ b/crates/bevy_reflect/src/set.rs @@ -67,6 +67,11 @@ pub trait Set: PartialReflect { /// After calling this function, `self` will be empty. fn drain(&mut self) -> Vec>; + /// Retain only the elements specified by the predicate. + /// + /// In other words, remove all elements `e` for which `f(&e)` returns `false`. + fn retain(&mut self, f: &mut dyn FnMut(&dyn PartialReflect) -> bool); + /// Creates a new [`DynamicSet`] from this set. fn to_dynamic_set(&self) -> DynamicSet { let mut set = DynamicSet::default(); @@ -139,7 +144,7 @@ impl SetInfo { impl_generic_info_methods!(generics); } -/// An ordered set of reflected values. +/// An unordered set of reflected values. #[derive(Default)] pub struct DynamicSet { represented_type: Option<&'static TypeInfo>, @@ -205,6 +210,10 @@ impl Set for DynamicSet { self.hash_table.drain().collect::>() } + fn retain(&mut self, f: &mut dyn FnMut(&dyn PartialReflect) -> bool) { + self.hash_table.retain(move |value| f(&**value)); + } + fn insert_boxed(&mut self, value: Box) -> bool { assert_eq!( value.reflect_partial_eq(&*value), @@ -441,27 +450,23 @@ pub fn set_debug(dyn_set: &dyn Set, f: &mut Formatter<'_>) -> core::fmt::Result /// Applies the elements of reflected set `b` to the corresponding elements of set `a`. /// /// If a value from `b` does not exist in `a`, the value is cloned and inserted. +/// If a value from `a` does not exist in `b`, the value is removed. /// /// # Panics /// /// This function panics if `b` is not a reflected set. #[inline] pub fn set_apply(a: &mut M, b: &dyn PartialReflect) { - if let ReflectRef::Set(set_value) = b.reflect_ref() { - for b_value in set_value.iter() { - if a.get(b_value).is_none() { - a.insert_boxed(b_value.to_dynamic()); - } - } - } else { - panic!("Attempted to apply a non-set type to a set type."); + if let Err(err) = set_try_apply(a, b) { + panic!("{err}"); } } /// Tries to apply the elements of reflected set `b` to the corresponding elements of set `a` /// and returns a Result. /// -/// If a key from `b` does not exist in `a`, the value is cloned and inserted. +/// If a value from `b` does not exist in `a`, the value is cloned and inserted. +/// If a value from `a` does not exist in `b`, the value is removed. /// /// # Errors /// @@ -476,12 +481,15 @@ pub fn set_try_apply(a: &mut S, b: &dyn PartialReflect) -> Result<(), Ap a.insert_boxed(b_value.to_dynamic()); } } + a.retain(&mut |value| set_value.get(value).is_some()); Ok(()) } #[cfg(test)] mod tests { + use crate::{PartialReflect, Set}; + use super::DynamicSet; use alloc::string::{String, ToString}; @@ -505,4 +513,21 @@ mod tests { assert_eq!(expected[index], value); } } + + #[test] + fn apply() { + let mut map_a = DynamicSet::default(); + map_a.insert(0); + map_a.insert(1); + + let mut map_b = DynamicSet::default(); + map_b.insert(1); + map_b.insert(2); + + map_a.apply(&map_b); + + assert!(map_a.get(&0).is_none()); + assert_eq!(map_a.get(&1).unwrap().try_downcast_ref(), Some(&1)); + assert_eq!(map_a.get(&2).unwrap().try_downcast_ref(), Some(&2)); + } } diff --git a/crates/bevy_reflect/src/std_traits.rs b/crates/bevy_reflect/src/std_traits.rs index cad001132b..9b7f46c300 100644 --- a/crates/bevy_reflect/src/std_traits.rs +++ b/crates/bevy_reflect/src/std_traits.rs @@ -1,3 +1,5 @@ +//! Module containing the [`ReflectDefault`] type. + use crate::{FromType, Reflect}; use alloc::boxed::Box; @@ -10,6 +12,7 @@ pub struct ReflectDefault { } impl ReflectDefault { + /// Returns the default value for a type. pub fn default(&self) -> Box { (self.default)() } diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 4346f55e27..e419947b3a 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -71,6 +71,7 @@ pub trait Struct: PartialReflect { /// Returns an iterator over the values of the reflectable fields for this struct. fn iter_fields(&self) -> FieldIter; + /// Creates a new [`DynamicStruct`] from this struct. fn to_dynamic_struct(&self) -> DynamicStruct { let mut dynamic_struct = DynamicStruct::default(); dynamic_struct.set_represented_type(self.get_represented_type_info()); @@ -192,6 +193,7 @@ pub struct FieldIter<'a> { } impl<'a> FieldIter<'a> { + /// Creates a new [`FieldIter`]. pub fn new(value: &'a dyn Struct) -> Self { FieldIter { struct_val: value, diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 8bdd08099b..97da69b5e2 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -76,6 +76,7 @@ pub struct TupleFieldIter<'a> { } impl<'a> TupleFieldIter<'a> { + /// Creates a new [`TupleFieldIter`]. pub fn new(value: &'a dyn Tuple) -> Self { TupleFieldIter { tuple: value, @@ -648,17 +649,29 @@ macro_rules! impl_reflect_tuple { } impl_reflect_tuple! {} + impl_reflect_tuple! {0: A} + impl_reflect_tuple! {0: A, 1: B} + impl_reflect_tuple! {0: A, 1: B, 2: C} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L} macro_rules! impl_type_path_tuple { diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index ab5b99a96b..cceab9904e 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -146,6 +146,7 @@ pub struct TupleStructFieldIter<'a> { } impl<'a> TupleStructFieldIter<'a> { + /// Creates a new [`TupleStructFieldIter`]. pub fn new(value: &'a dyn TupleStruct) -> Self { TupleStructFieldIter { tuple_struct: value, diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 1a3be15c36..122ace0293 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -169,7 +169,9 @@ pub enum TypeInfoError { /// [kind]: ReflectKind #[error("kind mismatch: expected {expected:?}, received {received:?}")] KindMismatch { + /// Expected kind. expected: ReflectKind, + /// Received kind. received: ReflectKind, }, } @@ -183,7 +185,7 @@ pub enum TypeInfoError { /// 3. [`PartialReflect::get_represented_type_info`] /// 4. [`TypeRegistry::get_type_info`] /// -/// Each return a static reference to [`TypeInfo`], but they all have their own use cases. +/// Each returns a static reference to [`TypeInfo`], but they all have their own use cases. /// For example, if you know the type at compile time, [`Typed::type_info`] is probably /// the simplest. If you have a `dyn Reflect` you can use [`DynamicTyped::reflect_type_info`]. /// If all you have is a `dyn PartialReflect`, you'll probably want [`PartialReflect::get_represented_type_info`]. @@ -199,14 +201,40 @@ pub enum TypeInfoError { /// [type path]: TypePath::type_path #[derive(Debug, Clone)] pub enum TypeInfo { + /// Type information for a [struct-like] type. + /// + /// [struct-like]: crate::Struct Struct(StructInfo), + /// Type information for a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: crate::TupleStruct TupleStruct(TupleStructInfo), + /// Type information for a [tuple-like] type. + /// + /// [tuple-like]: crate::Tuple Tuple(TupleInfo), + /// Type information for a [list-like] type. + /// + /// [list-like]: crate::List List(ListInfo), + /// Type information for an [array-like] type. + /// + /// [array-like]: crate::Array Array(ArrayInfo), + /// Type information for a [map-like] type. + /// + /// [map-like]: crate::Map Map(MapInfo), + /// Type information for a [set-like] type. + /// + /// [set-like]: crate::Set Set(SetInfo), + /// Type information for an [enum-like] type. + /// + /// [enum-like]: crate::Enum Enum(EnumInfo), + /// Type information for an opaque type - see the [`OpaqueInfo`] docs for + /// a discussion of opaque types. Opaque(OpaqueInfo), } @@ -557,6 +585,7 @@ pub struct OpaqueInfo { } impl OpaqueInfo { + /// Creates a new [`OpaqueInfo`]. pub fn new() -> Self { Self { ty: Type::of::(), diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 5827ebdac5..a20074b827 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -38,6 +38,7 @@ pub struct TypeRegistry { /// A synchronized wrapper around a [`TypeRegistry`]. #[derive(Clone, Default)] pub struct TypeRegistryArc { + /// The wrapped [`TypeRegistry`]. pub internal: Arc>, } @@ -313,6 +314,7 @@ impl TypeRegistry { data.insert(D::from_type()); } + /// Whether the type with given [`TypeId`] has been registered in this registry. pub fn contains(&self, type_id: TypeId) -> bool { self.registrations.contains_key(&type_id) } @@ -684,8 +686,10 @@ impl Clone for TypeRegistration { /// /// [crate-level documentation]: crate pub trait TypeData: Downcast + Send + Sync { + /// Creates a type-erased clone of this value. fn clone_type_data(&self) -> Box; } + impl_downcast!(TypeData); impl TypeData for T @@ -702,6 +706,7 @@ where /// This is used by the `#[derive(Reflect)]` macro to generate an implementation /// of [`TypeData`] to pass to [`TypeRegistration::insert`]. pub trait FromType { + /// Creates an instance of `Self` for type `T`. fn from_type() -> Self; } @@ -746,6 +751,8 @@ impl ReflectSerialize { /// [`FromType::from_type`]. #[derive(Clone)] pub struct ReflectDeserialize { + /// Function used by [`ReflectDeserialize::deserialize`] to + /// perform deserialization. pub func: fn( deserializer: &mut dyn erased_serde::Deserializer, ) -> Result, erased_serde::Error>, diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index 5735a29dbe..db8416bd6c 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -16,6 +16,7 @@ use core::{ /// /// [`Non`]: NonGenericTypeCell pub trait TypedProperty: sealed::Sealed { + /// The type of the value stored in [`GenericTypeCell`]. type Stored: 'static; } @@ -201,7 +202,7 @@ impl Default for NonGenericTypeCell { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("my_crate::foo::Foo<{}>", T::type_path())) /// } -/// +/// /// fn short_type_path() -> &'static str { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("Foo<{}>", T::short_type_path())) diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index ca84c2916e..7fc8d2b4de 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_remote" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "The Bevy Remote Protocol" homepage = "https://bevy.org" @@ -9,23 +9,27 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["http"] +default = ["http", "bevy_asset"] http = ["dep:async-io", "dep:smol-hyper"] +bevy_asset = ["dep:bevy_asset"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", features = [ "serialize", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", features = [ + "debug", +] } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", optional = true } # other anyhow = "1" diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 62ba8af661..615fdecc14 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -24,7 +24,10 @@ use serde_json::{Map, Value}; use crate::{ error_codes, - schemas::{json_schema::JsonSchemaBevyType, open_rpc::OpenRpcDocument}, + schemas::{ + json_schema::{export_type, JsonSchemaBevyType}, + open_rpc::OpenRpcDocument, + }, BrpError, BrpResult, }; @@ -522,7 +525,7 @@ pub fn process_remote_get_resource_request( else { return Err(BrpError { code: error_codes::RESOURCE_ERROR, - message: format!("Resource `{}` could not be serialized", resource_path), + message: format!("Resource `{resource_path}` could not be serialized"), data: None, }); }; @@ -696,7 +699,7 @@ fn reflect_component( else { return Err(BrpError { code: error_codes::COMPONENT_ERROR, - message: format!("Component `{}` could not be serialized", component_path), + message: format!("Component `{component_path}` could not be serialized"), data: None, }); }; @@ -1130,7 +1133,7 @@ pub fn process_remote_list_request(In(params): In>, world: &World) let Some(component_info) = world.components().get_info(component_id) else { continue; }; - response.push(component_info.name().to_owned()); + response.push(component_info.name().to_string()); } } // If `None`, list all registered components. @@ -1189,7 +1192,7 @@ pub fn process_remote_list_watching_request( let Some(component_info) = world.components().get_info(component_id) else { continue; }; - response.added.push(component_info.name().to_owned()); + response.added.push(component_info.name().to_string()); } } @@ -1202,7 +1205,7 @@ pub fn process_remote_list_watching_request( let Some(component_info) = world.components().get_info(*component_id) else { continue; }; - response.removed.push(component_info.name().to_owned()); + response.removed.push(component_info.name().to_string()); } } } @@ -1223,24 +1226,27 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br Some(params) => parse(params)?, }; + let extra_info = world.resource::(); let types = world.resource::(); let types = types.read(); let schemas = types .iter() - .map(crate::schemas::json_schema::export_type) - .filter(|(_, schema)| { - if let Some(crate_name) = &schema.crate_name { + .filter_map(|type_reg| { + let path_table = type_reg.type_info().type_path_table(); + if let Some(crate_name) = &path_table.crate_name() { if !filter.with_crates.is_empty() && !filter.with_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } if !filter.without_crates.is_empty() && filter.without_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } } + let (id, schema) = export_type(type_reg, extra_info); + if !filter.type_limit.with.is_empty() && !filter .type_limit @@ -1248,7 +1254,7 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } if !filter.type_limit.without.is_empty() && filter @@ -1257,10 +1263,9 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } - - true + Some((id.to_string(), schema)) }) .collect::>(); diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 97b2e453e7..348be8089d 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -364,6 +364,8 @@ //! [fully-qualified type names]: bevy_reflect::TypePath::type_path //! [fully-qualified type name]: bevy_reflect::TypePath::type_path +extern crate alloc; + use async_channel::{Receiver, Sender}; use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; @@ -539,6 +541,7 @@ impl Plugin for RemotePlugin { .insert_after(Last, RemoteLast); app.insert_resource(remote_methods) + .init_resource::() .init_resource::() .add_systems(PreStartup, setup_mailbox_channel) .configure_sets( diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index 3fcc588f92..4e56625bc8 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -1,47 +1,63 @@ //! Module with JSON Schema type for Bevy Registry Types. //! It tries to follow this standard: -use bevy_ecs::reflect::{ReflectComponent, ReflectResource}; +use alloc::borrow::Cow; use bevy_platform::collections::HashMap; use bevy_reflect::{ - prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, - TypeInfo, TypeRegistration, VariantInfo, + GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry, + VariantInfo, }; use core::any::TypeId; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -/// Exports schema info for a given type -pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { - (reg.type_info().type_path().to_owned(), reg.into()) -} +use crate::schemas::SchemaTypesMetadata; -fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { - // Vec could be moved to allow registering more types by game maker. - let registered_reflect_types: [(TypeId, &str); 5] = [ - { (TypeId::of::(), "Component") }, - { (TypeId::of::(), "Resource") }, - { (TypeId::of::(), "Default") }, - { (TypeId::of::(), "Serialize") }, - { (TypeId::of::(), "Deserialize") }, - ]; - let mut result = Vec::new(); - for (id, name) in registered_reflect_types { - if reg.data_by_id(id).is_some() { - result.push(name.to_owned()); - } +/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType` +pub trait TypeRegistrySchemaReader { + /// Export type JSON Schema. + fn export_type_json_schema( + &self, + extra_info: &SchemaTypesMetadata, + ) -> Option { + self.export_type_json_schema_for_id(extra_info, TypeId::of::()) } - result + /// Export type JSON Schema. + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option; } -impl From<&TypeRegistration> for JsonSchemaBevyType { - fn from(reg: &TypeRegistration) -> Self { +impl TypeRegistrySchemaReader for TypeRegistry { + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option { + let type_reg = self.get(type_id)?; + Some((type_reg, extra_info).into()) + } +} + +/// Exports schema info for a given type +pub fn export_type( + reg: &TypeRegistration, + metadata: &SchemaTypesMetadata, +) -> (Cow<'static, str>, JsonSchemaBevyType) { + (reg.type_info().type_path().into(), (reg, metadata).into()) +} + +impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { + fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self { + let (reg, metadata) = value; let t = reg.type_info(); let binding = t.type_path_table(); let short_path = binding.short_path(); let type_path = binding.path(); let mut typed_schema = JsonSchemaBevyType { - reflect_types: get_registered_reflect_types(reg), + reflect_types: metadata.get_registered_reflect_types(reg), short_path: short_path.to_owned(), type_path: type_path.to_owned(), crate_name: binding.crate_name().map(str::to_owned), @@ -351,8 +367,12 @@ impl SchemaJsonReference for &NamedField { #[cfg(test)] mod tests { use super::*; + use bevy_ecs::prelude::ReflectComponent; + use bevy_ecs::prelude::ReflectResource; + use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; - use bevy_reflect::Reflect; + use bevy_reflect::prelude::ReflectDefault; + use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; #[test] fn reflect_export_struct() { @@ -373,7 +393,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), @@ -418,7 +438,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -453,7 +473,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), "Should not be a component" @@ -466,6 +486,62 @@ mod tests { assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); } + #[test] + fn reflect_struct_with_custom_type_data() { + #[derive(Reflect, Default, Deserialize, Serialize)] + #[reflect(Default)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + #[derive(Clone)] + pub struct ReflectCustomData; + + impl bevy_reflect::FromType for ReflectCustomData { + fn from_type() -> Self { + ReflectCustomData + } + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + register.register_type_data::(); + } + let mut metadata = SchemaTypesMetadata::default(); + metadata.map_type_data::("CustomData"); + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration, &metadata); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a component" + ); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a resource" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have default" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have CustomData" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); + } + #[test] fn reflect_export_tuple_struct() { #[derive(Reflect, Component, Default, Deserialize, Serialize)] @@ -482,7 +558,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -513,7 +589,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); let value = json!({ "shortPath": "Foo", @@ -538,6 +614,31 @@ mod tests { "a" ] }); - assert_eq!(schema_as_value, value); + assert_normalized_values(schema_as_value, value); + } + + /// This function exist to avoid false failures due to ordering differences between `serde_json` values. + fn assert_normalized_values(mut one: Value, mut two: Value) { + normalize_json(&mut one); + normalize_json(&mut two); + assert_eq!(one, two); + + /// Recursively sorts arrays in a `serde_json::Value` + fn normalize_json(value: &mut Value) { + match value { + Value::Array(arr) => { + for v in arr.iter_mut() { + normalize_json(v); + } + arr.sort_by_key(ToString::to_string); // Sort by stringified version + } + Value::Object(map) => { + for (_k, v) in map.iter_mut() { + normalize_json(v); + } + } + _ => {} + } + } } } diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs index 7104fd5547..10cb2e9421 100644 --- a/crates/bevy_remote/src/schemas/mod.rs +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -1,4 +1,68 @@ //! Module with schemas used for various BRP endpoints +use bevy_ecs::{ + reflect::{ReflectComponent, ReflectResource}, + resource::Resource, +}; +use bevy_platform::collections::HashMap; +use bevy_reflect::{ + prelude::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize, TypeData, + TypeRegistration, +}; +use core::any::TypeId; pub mod json_schema; pub mod open_rpc; + +/// Holds mapping of reflect [type data](TypeData) to strings, +/// later on used in Bevy Json Schema. +#[derive(Debug, Resource, Reflect)] +#[reflect(Resource)] +pub struct SchemaTypesMetadata { + /// Type Data id mapping to strings. + pub type_data_map: HashMap, +} + +impl Default for SchemaTypesMetadata { + fn default() -> Self { + let mut data_types = Self { + type_data_map: Default::default(), + }; + data_types.map_type_data::("Component"); + data_types.map_type_data::("Resource"); + data_types.map_type_data::("Default"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("Asset"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("AssetHandle"); + data_types.map_type_data::("Serialize"); + data_types.map_type_data::("Deserialize"); + data_types + } +} + +impl SchemaTypesMetadata { + /// Map `TypeId` of `TypeData` to string + pub fn map_type_data(&mut self, name: impl Into) { + self.type_data_map.insert(TypeId::of::(), name.into()); + } + + /// Build reflect types list for a given type registration + pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec { + self.type_data_map + .iter() + .filter_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone()))) + .collect() + } + + /// Checks if slice contains string value that matches checked `TypeData` + pub fn has_type_data(&self, types_string_slice: &[String]) -> bool { + self.has_type_data_by_id(TypeId::of::(), types_string_slice) + } + + /// Checks if slice contains string value that matches checked `TypeData` by id. + pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool { + self.type_data_map + .get(&id) + .is_some_and(|data_s| types_string_slice.iter().any(|e| e.eq(data_s))) + } +} diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index d9775e9c8f..a657da4f0e 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_render" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides rendering functionality for Bevy Engine" homepage = "https://bevy.org" @@ -21,11 +21,14 @@ keywords = ["bevy"] # wgpu-types = { git = "https://github.com/gfx-rs/wgpu", rev = "..." } decoupled_naga = [] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = ["bevy_image/compressed_image_saver"] + # Texture formats (require more than just image support) basis-universal = ["bevy_image/basis-universal"] exr = ["bevy_image/exr"] hdr = ["bevy_image/hdr"] -ktx2 = ["dep:ktx2", "bevy_image/ktx2"] +ktx2 = ["bevy_image/ktx2"] multi_threaded = ["bevy_tasks/multi_threaded"] @@ -46,32 +49,34 @@ ci_limits = [] webgl = ["wgpu/webgl"] webgpu = ["wgpu/webgpu"] detailed_trace = [] +## Adds serialization support through `serde`. +serialize = ["bevy_mesh/serialize"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ "serialize", "wgpu-types", ] } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render_macros = { path = "macros", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", features = [ +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render_macros = { path = "macros", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", features = [ "wgpu_wrapper", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -80,26 +85,27 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea image = { version = "0.25.2", default-features = false } # misc -codespan-reporting = "0.11.0" +codespan-reporting = "0.12.0" # `fragile-send-sync-non-atomic-wasm` feature means we can't use Wasm threads for rendering # It is enabled for now to avoid having to do a significant overhaul of the renderer just for wasm. # When the 'atomics' feature is enabled `fragile-send-sync-non-atomic` does nothing # and Bevy instead wraps `wgpu` types to verify they are not used off their origin thread. -wgpu = { version = "24", default-features = false, features = [ +wgpu = { version = "25", default-features = false, features = [ "wgsl", "dx12", "metal", + "vulkan", + "gles", "naga-ir", "fragile-send-sync-non-atomic-wasm", ] } -naga = { version = "24", features = ["wgsl-in"] } +naga = { version = "25", features = ["wgsl-in"] } serde = { version = "1", features = ["derive"] } bytemuck = { version = "1.5", features = ["derive", "must_cast"] } downcast-rs = { version = "2", default-features = false, features = ["std"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } futures-lite = "2.0.1" -ktx2 = { version = "0.4.0", optional = true } encase = { version = "0.10", features = ["glam"] } # For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. profiling = { version = "1", features = [ @@ -119,7 +125,7 @@ wesl = { version = "0.1.2", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. -naga_oil = { version = "0.17.1", default-features = false, features = [ +naga_oil = { version = "0.18", default-features = false, features = [ "test_shader", ] } @@ -127,7 +133,7 @@ naga_oil = { version = "0.17.1", default-features = false, features = [ proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.17.1" +naga_oil = { version = "0.18" } js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', @@ -140,16 +146,16 @@ web-sys = { version = "0.3.67", features = [ ] } wasm-bindgen = "0.2" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index 74348598fe..016fe88765 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_render_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_render" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 4252929170..7bac6796ac 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -204,7 +204,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #bind_group_layout_entries.push( #render_path::render_resource::BindGroupLayoutEntry { binding: #binding_array_binding, - visibility: #render_path::render_resource::ShaderStages::all(), + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, ty: #render_path::render_resource::BindingType::Buffer { ty: #uniform_binding_type, has_dynamic_offset: false, @@ -253,7 +253,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #bind_group_layout_entries.push( #render_path::render_resource::BindGroupLayoutEntry { binding: #binding_array_binding, - visibility: #render_path::render_resource::ShaderStages::all(), + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, ty: #render_path::render_resource::BindingType::Buffer { ty: #uniform_binding_type, has_dynamic_offset: false, @@ -279,7 +279,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #bind_group_layout_entries.push( #render_path::render_resource::BindGroupLayoutEntry { binding: #binding_index, - visibility: #render_path::render_resource::ShaderStages::all(), + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, ty: #render_path::render_resource::BindingType::Buffer { ty: #uniform_binding_type, has_dynamic_offset: false, @@ -519,7 +519,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #bind_group_layout_entries.push( #render_path::render_resource::BindGroupLayoutEntry { binding: #binding_array_binding, - visibility: #render_path::render_resource::ShaderStages::all(), + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, ty: #render_path::render_resource::BindingType::Buffer { ty: #render_path::render_resource::BufferBindingType::Storage { read_only: #read_only @@ -834,7 +834,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #bind_group_layout_entries.push( #render_path::render_resource::BindGroupLayoutEntry { binding: #binding_index, - visibility: #render_path::render_resource::ShaderStages::all(), + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, ty: #render_path::render_resource::BindingType::Buffer { ty: #uniform_binding_type, has_dynamic_offset: false, @@ -881,7 +881,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { non_bindless_binding_layouts.push(quote!{ #bind_group_layout_entries.push(#render_path::render_resource::BindGroupLayoutEntry { binding: #binding_index, - visibility: #render_path::render_resource::ShaderStages::all(), + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, ty: #render_path::render_resource::BindingType::Buffer { ty: #uniform_binding_type, has_dynamic_offset: false, @@ -1337,7 +1337,13 @@ impl VisibilityFlags { impl ShaderStageVisibility { fn hygienic_quote(&self, path: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { match self { - ShaderStageVisibility::All => quote! { #path::ShaderStages::all() }, + ShaderStageVisibility::All => quote! { + if cfg!(feature = "webgpu") { + todo!("Please use a more specific shader stage: https://github.com/gfx-rs/wgpu/issues/7708") + } else { + #path::ShaderStages::all() + } + }, ShaderStageVisibility::None => quote! { #path::ShaderStages::NONE }, ShaderStageVisibility::Flags(flags) => { let mut quoted = Vec::new(); diff --git a/crates/bevy_render/macros/src/extract_component.rs b/crates/bevy_render/macros/src/extract_component.rs index 2bfd0e0e11..8526f7b889 100644 --- a/crates/bevy_render/macros/src/extract_component.rs +++ b/crates/bevy_render/macros/src/extract_component.rs @@ -43,7 +43,7 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream { type QueryFilter = #filter; type Out = Self; - fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_render/src/bindless.wgsl b/crates/bevy_render/src/bindless.wgsl index 05517a1746..717e9c1047 100644 --- a/crates/bevy_render/src/bindless.wgsl +++ b/crates/bevy_render/src/bindless.wgsl @@ -16,22 +16,22 @@ // Binding 0 is the bindless index table. // Filtering samplers. -@group(2) @binding(1) var bindless_samplers_filtering: binding_array; +@group(3) @binding(1) var bindless_samplers_filtering: binding_array; // Non-filtering samplers (nearest neighbor). -@group(2) @binding(2) var bindless_samplers_non_filtering: binding_array; +@group(3) @binding(2) var bindless_samplers_non_filtering: binding_array; // Comparison samplers (typically for shadow mapping). -@group(2) @binding(3) var bindless_samplers_comparison: binding_array; +@group(3) @binding(3) var bindless_samplers_comparison: binding_array; // 1D textures. -@group(2) @binding(4) var bindless_textures_1d: binding_array>; +@group(3) @binding(4) var bindless_textures_1d: binding_array>; // 2D textures. -@group(2) @binding(5) var bindless_textures_2d: binding_array>; +@group(3) @binding(5) var bindless_textures_2d: binding_array>; // 2D array textures. -@group(2) @binding(6) var bindless_textures_2d_array: binding_array>; +@group(3) @binding(6) var bindless_textures_2d_array: binding_array>; // 3D textures. -@group(2) @binding(7) var bindless_textures_3d: binding_array>; +@group(3) @binding(7) var bindless_textures_3d: binding_array>; // Cubemap textures. -@group(2) @binding(8) var bindless_textures_cube: binding_array>; +@group(3) @binding(8) var bindless_textures_cube: binding_array>; // Cubemap array textures. -@group(2) @binding(9) var bindless_textures_cube_array: binding_array>; +@group(3) @binding(9) var bindless_textures_cube_array: binding_array>; #endif // BINDLESS diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index fd3b8cb4b2..2732a44316 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -5,7 +5,7 @@ use super::{ClearColorConfig, Projection}; use crate::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, - camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews}, + camera::{ManualTextureViewHandle, ManualTextureViews}, primitives::Frustum, render_asset::RenderAssets, render_graph::{InternedRenderSubGraph, RenderSubGraph}, @@ -311,8 +311,8 @@ pub enum ViewportConversionError { #[error("computed coordinate beyond `Camera`'s far plane")] PastFarPlane, /// The Normalized Device Coordinates could not be computed because the `camera_transform`, the - /// `world_position`, or the projection matrix defined by [`CameraProjection`] contained `NAN` - /// (see [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]). + /// `world_position`, or the projection matrix defined by [`Projection`] contained `NAN` (see + /// [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]). #[error("found NaN while computing NDC")] InvalidData, } @@ -490,7 +490,7 @@ impl Camera { .map(|t: &RenderTargetInfo| t.scale_factor) } - /// The projection matrix computed using this camera's [`CameraProjection`]. + /// The projection matrix computed using this camera's [`Projection`]. #[inline] pub fn clip_from_view(&self) -> Mat4 { self.computed.clip_from_view @@ -601,8 +601,7 @@ impl Camera { rect_relative.y = 1.0 - rect_relative.y; let ndc = rect_relative * 2. - Vec2::ONE; - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.)); // Using EPSILON because an ndc with Z = 0 returns NaNs. let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON)); @@ -656,7 +655,7 @@ impl Camera { /// To get the coordinates in the render target's viewport dimensions, you should use /// [`world_to_viewport`](Self::world_to_viewport). /// - /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. + /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`Projection`] contain `NAN`. /// /// # Panics /// @@ -668,7 +667,7 @@ impl Camera { ) -> Option { // Build a transformation matrix to convert from world space to NDC using camera data let clip_from_world: Mat4 = - self.computed.clip_from_view * camera_transform.compute_matrix().inverse(); + self.computed.clip_from_view * camera_transform.to_matrix().inverse(); let ndc_space_coords: Vec3 = clip_from_world.project_point3(world_position); (!ndc_space_coords.is_nan()).then_some(ndc_space_coords) @@ -682,15 +681,14 @@ impl Camera { /// To get the world space coordinates with the viewport position, you should use /// [`world_to_viewport`](Self::world_to_viewport). /// - /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. + /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`Projection`] contain `NAN`. /// /// # Panics /// /// Will panic if the projection matrix is invalid (has a determinant of 0) and `glam_assert` is enabled. pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option { // Build a transformation matrix to convert from NDC to world space using camera data - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_space_coords = ndc_to_world.project_point3(ndc); diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index a7796a1d1a..9fa8831432 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -1,9 +1,9 @@ use core::fmt::Debug; +use core::ops::{Deref, DerefMut}; use crate::{primitives::Frustum, view::VisibilitySystems}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_asset::AssetEventSystems; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; @@ -93,8 +93,7 @@ pub trait CameraProjection { /// This code is called by [`update_frusta`](crate::view::visibility::update_frusta) system /// for each camera to update its frustum. fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum { - let clip_from_world = - self.get_clip_from_view() * camera_transform.compute_matrix().inverse(); + let clip_from_world = self.get_clip_from_view() * camera_transform.to_matrix().inverse(); Frustum::from_clip_from_world_custom_far( &clip_from_world, &camera_transform.translation(), @@ -132,11 +131,10 @@ mod sealed { /// custom projection. /// /// The contained dynamic object can be downcast into a static type using [`CustomProjection::get`]. -#[derive(Component, Debug, Reflect, Deref, DerefMut)] +#[derive(Debug, Reflect)] #[reflect(Default, Clone)] pub struct CustomProjection { #[reflect(ignore)] - #[deref] dyn_projection: Box, } @@ -205,6 +203,20 @@ impl CustomProjection { } } +impl Deref for CustomProjection { + type Target = dyn CameraProjection; + + fn deref(&self) -> &Self::Target { + self.dyn_projection.as_ref() + } +} + +impl DerefMut for CustomProjection { + fn deref_mut(&mut self) -> &mut Self::Target { + self.dyn_projection.as_mut() + } +} + /// Component that defines how to compute a [`Camera`]'s projection matrix. /// /// Common projections, like perspective and orthographic, are provided out of the box to handle the @@ -241,7 +253,7 @@ impl Projection { // that, say, the `Debug` implementation is missing. Wrapping these traits behind a super // trait or some other indirection will make the errors harder to understand. // - // For example, we don't use the `DynCameraProjection`` trait bound, because it is not the + // For example, we don't use the `DynCameraProjection` trait bound, because it is not the // trait the user should be implementing - they only need to worry about implementing // `CameraProjection`. P: CameraProjection + Debug + Send + Sync + Clone + 'static, @@ -252,44 +264,24 @@ impl Projection { } } -impl CameraProjection for Projection { - fn get_clip_from_view(&self) -> Mat4 { +impl Deref for Projection { + type Target = dyn CameraProjection; + + fn deref(&self) -> &Self::Target { match self { - Projection::Perspective(projection) => projection.get_clip_from_view(), - Projection::Orthographic(projection) => projection.get_clip_from_view(), - Projection::Custom(projection) => projection.get_clip_from_view(), + Projection::Perspective(projection) => projection, + Projection::Orthographic(projection) => projection, + Projection::Custom(projection) => projection.deref(), } } +} - fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { +impl DerefMut for Projection { + fn deref_mut(&mut self) -> &mut Self::Target { match self { - Projection::Perspective(projection) => projection.get_clip_from_view_for_sub(sub_view), - Projection::Orthographic(projection) => projection.get_clip_from_view_for_sub(sub_view), - Projection::Custom(projection) => projection.get_clip_from_view_for_sub(sub_view), - } - } - - fn update(&mut self, width: f32, height: f32) { - match self { - Projection::Perspective(projection) => projection.update(width, height), - Projection::Orthographic(projection) => projection.update(width, height), - Projection::Custom(projection) => projection.update(width, height), - } - } - - fn far(&self) -> f32 { - match self { - Projection::Perspective(projection) => projection.far(), - Projection::Orthographic(projection) => projection.far(), - Projection::Custom(projection) => projection.far(), - } - } - - fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { - match self { - Projection::Perspective(projection) => projection.get_frustum_corners(z_near, z_far), - Projection::Orthographic(projection) => projection.get_frustum_corners(z_near, z_far), - Projection::Custom(projection) => projection.get_frustum_corners(z_near, z_far), + Projection::Perspective(projection) => projection, + Projection::Orthographic(projection) => projection, + Projection::Custom(projection) => projection.deref_mut(), } } } diff --git a/crates/bevy_render/src/diagnostic/mod.rs b/crates/bevy_render/src/diagnostic/mod.rs index 7f046036a9..197b9f4e7f 100644 --- a/crates/bevy_render/src/diagnostic/mod.rs +++ b/crates/bevy_render/src/diagnostic/mod.rs @@ -148,7 +148,7 @@ pub struct PassSpanGuard<'a, R: ?Sized, P> { } impl PassSpanGuard<'_, R, P> { - /// End the span. You have to provide the same encoder which was used to begin the span. + /// End the span. You have to provide the same pass which was used to begin the span. pub fn end(self, pass: &mut P) { self.recorder.end_pass_span(pass); core::mem::forget(self); diff --git a/crates/bevy_render/src/diagnostic/tracy_gpu.rs b/crates/bevy_render/src/diagnostic/tracy_gpu.rs index c059b8baa5..7a66db4ea6 100644 --- a/crates/bevy_render/src/diagnostic/tracy_gpu.rs +++ b/crates/bevy_render/src/diagnostic/tracy_gpu.rs @@ -1,7 +1,7 @@ use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue}; use tracy_client::{Client, GpuContext, GpuContextType}; use wgpu::{ - Backend, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Maintain, MapMode, + Backend, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, MapMode, PollType, QuerySetDescriptor, QueryType, QUERY_SIZE, }; @@ -14,7 +14,7 @@ pub fn new_tracy_gpu_context( Backend::Vulkan => GpuContextType::Vulkan, Backend::Dx12 => GpuContextType::Direct3D12, Backend::Gl => GpuContextType::OpenGL, - Backend::Metal | Backend::BrowserWebGpu | Backend::Empty => GpuContextType::Invalid, + Backend::Metal | Backend::BrowserWebGpu | Backend::Noop => GpuContextType::Invalid, }; let tracy_client = Client::running().unwrap(); @@ -60,7 +60,9 @@ fn initial_timestamp(device: &RenderDevice, queue: &RenderQueue) -> i64 { queue.submit([timestamp_encoder.finish(), copy_encoder.finish()]); map_buffer.slice(..).map_async(MapMode::Read, |_| ()); - device.poll(Maintain::Wait); + device + .poll(PollType::Wait) + .expect("Failed to poll device for map async"); let view = map_buffer.slice(..).get_mapped_range(); i64::from_le_bytes((*view).try_into().unwrap()) diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index e1f528d6ab..b7bb05e425 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -60,7 +60,7 @@ pub trait ExtractComponent: Component { // type Out: Component = Self; /// Defines how the component is transferred into the "render world". - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin prepares the components of the corresponding type for the GPU diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index a8e5a9ecbd..cd194b7c40 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -34,7 +34,7 @@ pub trait ExtractInstance: Send + Sync + Sized + 'static { type QueryFilter: QueryFilter; /// Defines how the component is transferred into the "render world". - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin extracts one or more components into the "render world" as diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index ac6b04ff0f..e97c758260 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -1,7 +1,8 @@ use crate::MainWorld; use bevy_ecs::{ - component::Tick, + component::{ComponentId, Tick}, prelude::*, + query::FilteredAccessSet, system::{ ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemParamValidationError, SystemState, @@ -71,14 +72,28 @@ where type State = ExtractState

; type Item<'w, 's> = Extract<'w, 's, P>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { let mut main_world = world.resource_mut::(); ExtractState { state: SystemState::new(&mut main_world), - main_world_state: Res::::init_state(world, system_meta), + main_world_state: Res::::init_state(world), } } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Res::::init_access( + &state.main_world_state, + system_meta, + component_access_set, + world, + ); + } + #[inline] unsafe fn validate_param( state: &mut Self::State, diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index c05861f3da..adcb883559 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -15,14 +15,14 @@ use async_channel::{Receiver, Sender}; use bevy_app::{App, Plugin}; use bevy_asset::Handle; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_ecs::{ change_detection::ResMut, entity::Entity, - event::Event, + event::EntityEvent, prelude::{Component, Resource, World}, system::{Query, Res}, }; +use bevy_ecs::{event::Event, schedule::IntoScheduleConfigs}; use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; @@ -96,7 +96,7 @@ impl Readback { /// /// The event contains the data as a `Vec`, which can be interpreted as the raw bytes of the /// requested buffer or texture. -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ReadbackComplete(pub Vec); diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index a20315c099..aa1e2da676 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -352,10 +352,12 @@ impl Plugin for RenderPlugin { backend_options: wgpu::BackendOptions { gl: wgpu::GlBackendOptions { gles_minor_version: settings.gles3_minor_version, + fence_behavior: wgpu::GlFenceBehavior::Normal, }, dx12: wgpu::Dx12BackendOptions { shader_compiler: settings.dx12_shader_compiler.clone(), }, + noop: wgpu::NoopBackendOptions { enable: false }, }, }); diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index d15468376f..c981e75cee 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -209,6 +209,7 @@ impl RenderAsset for RenderMesh { mesh: Self::SourceAsset, _: AssetId, (images, mesh_vertex_buffer_layouts): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let morph_targets = match mesh.morph_targets() { Some(mt) => { diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 1fa5758ca3..0a5ad3e4ec 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -73,6 +73,7 @@ pub trait RenderAsset: Send + Sync + 'static + Sized { source_asset: Self::SourceAsset, asset_id: AssetId, param: &mut SystemParamItem, + previous_asset: Option<&Self>, ) -> Result>; /// Called whenever the [`RenderAsset::SourceAsset`] has been removed. @@ -355,7 +356,8 @@ pub fn prepare_assets( 0 }; - match A::prepare_asset(extracted_asset, id, &mut param) { + let previous_asset = render_assets.get(id); + match A::prepare_asset(extracted_asset, id, &mut param, previous_asset) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); @@ -382,7 +384,7 @@ pub fn prepare_assets( // we remove previous here to ensure that if we are updating the asset then // any users will not see the old asset after a new asset is extracted, // even if the new asset is not yet ready or we are out of bytes to write. - render_assets.remove(id); + let previous_asset = render_assets.remove(id); let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { if bpf.exhausted() { @@ -394,7 +396,7 @@ pub fn prepare_assets( 0 }; - match A::prepare_asset(extracted_asset, id, &mut param) { + match A::prepare_asset(extracted_asset, id, &mut param, previous_asset.as_ref()) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 0a634c2598..4355892487 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -366,7 +366,7 @@ pub trait ViewNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError>; } diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 374dc6b330..39c6a074e6 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -168,14 +168,16 @@ impl DrawFunctions

{ /// ``` /// # use bevy_render::render_phase::SetItemPipeline; /// # struct SetMeshViewBindGroup; +/// # struct SetMeshViewBindingArrayBindGroup; /// # struct SetMeshBindGroup; /// # struct SetMaterialBindGroup(std::marker::PhantomData); /// # struct DrawMesh; /// pub type DrawMaterial = ( /// SetItemPipeline, /// SetMeshViewBindGroup<0>, -/// SetMeshBindGroup<1>, -/// SetMaterialBindGroup, +/// SetMeshViewBindingArrayBindGroup<1>, +/// SetMeshBindGroup<2>, +/// SetMaterialBindGroup, /// DrawMesh, /// ); /// ``` @@ -213,8 +215,8 @@ pub trait RenderCommand { /// issuing draw calls, etc.) via the [`TrackedRenderPass`]. fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, - entity: Option>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, + entity: Option>, param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult; @@ -246,8 +248,8 @@ macro_rules! render_command_tuple_impl { )] fn render<'w>( _item: &P, - ($($view,)*): ROQueryItem<'w, Self::ViewQuery>, - maybe_entities: Option>, + ($($view,)*): ROQueryItem<'w, '_, Self::ViewQuery>, + maybe_entities: Option>, ($($name,)*): SystemParamItem<'w, '_, Self::Param>, _pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index cff88bd355..17de8455da 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -133,12 +133,12 @@ impl Deref for BindGroup { /// In WGSL shaders, the binding would look like this: /// /// ```wgsl -/// @group(2) @binding(0) var color: vec4; -/// @group(2) @binding(1) var color_texture: texture_2d; -/// @group(2) @binding(2) var color_sampler: sampler; -/// @group(2) @binding(3) var storage_buffer: array; -/// @group(2) @binding(4) var raw_buffer: array; -/// @group(2) @binding(5) var storage_texture: texture_storage_2d; +/// @group(3) @binding(0) var color: vec4; +/// @group(3) @binding(1) var color_texture: texture_2d; +/// @group(3) @binding(2) var color_sampler: sampler; +/// @group(3) @binding(3) var storage_buffer: array; +/// @group(3) @binding(4) var raw_buffer: array; +/// @group(3) @binding(5) var storage_texture: texture_storage_2d; /// ``` /// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups /// are generally bound to group 2. @@ -261,7 +261,7 @@ impl Deref for BindGroup { /// roughness: f32, /// }; /// -/// @group(2) @binding(0) var material: CoolMaterial; +/// @group(3) @binding(0) var material: CoolMaterial; /// ``` /// /// Some less common scenarios will require "struct-level" attributes. These are the currently supported struct-level attributes: @@ -312,7 +312,7 @@ impl Deref for BindGroup { /// declaration: /// /// ```wgsl -/// @group(2) @binding(10) var material_array: binding_array; +/// @group(3) @binding(10) var material_array: binding_array; /// ``` /// /// On the other hand, if you write this declaration: @@ -325,7 +325,7 @@ impl Deref for BindGroup { /// Then Bevy produces a binding that matches this WGSL declaration instead: /// /// ```wgsl -/// @group(2) @binding(10) var material_array: array; +/// @group(3) @binding(10) var material_array: array; /// ``` /// /// * Just as with the structure-level `uniform` attribute, Bevy converts the @@ -338,7 +338,7 @@ impl Deref for BindGroup { /// this in WGSL in non-bindless mode: /// /// ```wgsl -/// @group(2) @binding(0) var material: StandardMaterial; +/// @group(3) @binding(0) var material: StandardMaterial; /// ``` /// /// * For efficiency reasons, `data` is generally preferred over `uniform` diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index cc8eb188de..847bb46f49 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -287,6 +287,12 @@ impl<'b> DynamicBindGroupEntries<'b> { } } + pub fn new() -> Self { + Self { + entries: Vec::new(), + } + } + pub fn extend_with_indices( mut self, entries: impl IntoIndexedBindingArray<'b, N>, diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index 41affa4349..17630b7dae 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -334,6 +334,13 @@ impl DynamicBindGroupLayoutEntries { } } + pub fn new(default_visibility: ShaderStages) -> Self { + Self { + default_visibility, + entries: Vec::new(), + } + } + pub fn extend_with_indices( mut self, entries: impl IntoIndexedBindGroupLayoutEntryBuilderArray, @@ -570,6 +577,16 @@ pub mod binding_types { } pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { - BindingType::AccelerationStructure.into_bind_group_layout_entry_builder() + BindingType::AccelerationStructure { + vertex_return: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn acceleration_structure_vertex_return() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure { + vertex_return: true, + } + .into_bind_group_layout_entry_builder() } } diff --git a/crates/bevy_render/src/render_resource/bindless.rs b/crates/bevy_render/src/render_resource/bindless.rs index 64a0fa2c1f..dc3bf00eee 100644 --- a/crates/bevy_render/src/render_resource/bindless.rs +++ b/crates/bevy_render/src/render_resource/bindless.rs @@ -243,35 +243,65 @@ pub fn create_bindless_bind_group_layout_entries( false, NonZeroU64::new(bindless_index_table_length as u64 * size_of::() as u64), ) - .build(*bindless_index_table_binding_number, ShaderStages::all()), + .build( + *bindless_index_table_binding_number, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), // Continue with the common bindless resource arrays. sampler(SamplerBindingType::Filtering) .count(bindless_slab_resource_limit) - .build(1, ShaderStages::all()), + .build( + 1, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), sampler(SamplerBindingType::NonFiltering) .count(bindless_slab_resource_limit) - .build(2, ShaderStages::all()), + .build( + 2, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), sampler(SamplerBindingType::Comparison) .count(bindless_slab_resource_limit) - .build(3, ShaderStages::all()), + .build( + 3, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), texture_1d(TextureSampleType::Float { filterable: true }) .count(bindless_slab_resource_limit) - .build(4, ShaderStages::all()), + .build( + 4, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), texture_2d(TextureSampleType::Float { filterable: true }) .count(bindless_slab_resource_limit) - .build(5, ShaderStages::all()), + .build( + 5, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), texture_2d_array(TextureSampleType::Float { filterable: true }) .count(bindless_slab_resource_limit) - .build(6, ShaderStages::all()), + .build( + 6, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), texture_3d(TextureSampleType::Float { filterable: true }) .count(bindless_slab_resource_limit) - .build(7, ShaderStages::all()), + .build( + 7, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), texture_cube(TextureSampleType::Float { filterable: true }) .count(bindless_slab_resource_limit) - .build(8, ShaderStages::all()), + .build( + 8, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), texture_cube_array(TextureSampleType::Float { filterable: true }) .count(bindless_slab_resource_limit) - .build(9, ShaderStages::all()), + .build( + 9, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), ] } diff --git a/crates/bevy_render/src/render_resource/buffer_vec.rs b/crates/bevy_render/src/render_resource/buffer_vec.rs index 4e6c787fba..1fdb26655d 100644 --- a/crates/bevy_render/src/render_resource/buffer_vec.rs +++ b/crates/bevy_render/src/render_resource/buffer_vec.rs @@ -183,6 +183,31 @@ impl RawBufferVec { } } + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// Before queuing the write, a [`reserve`](RawBufferVec::reserve) operation + /// is executed. + /// + /// This will only write the data contained in the given range. It is useful if you only want + /// to update a part of the buffer. + pub fn write_buffer_range( + &mut self, + device: &RenderDevice, + render_queue: &RenderQueue, + range: core::ops::Range, + ) { + if self.values.is_empty() { + return; + } + self.reserve(self.values.len(), device); + if let Some(buffer) = &self.buffer { + // Cast only the bytes we need to write + let bytes: &[u8] = must_cast_slice(&self.values[range.start..range.end]); + render_queue.write_buffer(buffer, (range.start * self.item_size) as u64, bytes); + } + } + /// Reduces the length of the buffer. pub fn truncate(&mut self, len: usize) { self.values.truncate(len); @@ -389,6 +414,31 @@ where queue.write_buffer(buffer, 0, &self.data); } + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// Before queuing the write, a [`reserve`](BufferVec::reserve) operation + /// is executed. + /// + /// This will only write the data contained in the given range. It is useful if you only want + /// to update a part of the buffer. + pub fn write_buffer_range( + &mut self, + device: &RenderDevice, + render_queue: &RenderQueue, + range: core::ops::Range, + ) { + if self.data.is_empty() { + return; + } + let item_size = u64::from(T::min_size()) as usize; + self.reserve(self.data.len() / item_size, device); + if let Some(buffer) = &self.buffer { + let bytes = &self.data[range.start..range.end]; + render_queue.write_buffer(buffer, (range.start * item_size) as u64, bytes); + } + } + /// Reduces the length of the buffer. pub fn truncate(&mut self, len: usize) { self.data.truncate(u64::from(T::min_size()) as usize * len); diff --git a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs index 195920ee0c..0c5bf36bf6 100644 --- a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs +++ b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs @@ -14,6 +14,7 @@ use wgpu::{BindingResource, BufferUsages}; /// Trait for types able to go in a [`GpuArrayBuffer`]. pub trait GpuArrayBufferable: ShaderType + ShaderSize + WriteInto + Clone {} + impl GpuArrayBufferable for T {} /// Stores an array of elements to be transferred to the GPU and made accessible to shaders as a read-only array. diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index aecf27173d..09be66e840 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -49,18 +49,18 @@ pub use wgpu::{ ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, - FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, MapMode, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, MapMode, MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, - PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, - RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, - RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, - SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, - ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, - StencilOperation, StencilState, StorageTextureAccess, StoreOp, TexelCopyBufferInfo, - TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, - TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, - TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, - TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute, + PipelineLayoutDescriptor, PollType, PolygonMode, PrimitiveState, PrimitiveTopology, + PushConstantRange, RenderPassColorAttachment, RenderPassDepthStencilAttachment, + RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, + Sampler as WgpuSampler, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, + SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, + StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp, + TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, + TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags, + TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, + TextureViewDescriptor, TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, }; diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 416a83cd65..ebd3229636 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -873,7 +873,7 @@ impl PipelineCache { // TODO: Expose the rest of this somehow let compilation_options = PipelineCompilationOptions { - constants: &default(), + constants: &[], zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory, }; @@ -955,7 +955,7 @@ impl PipelineCache { entry_point: Some(&descriptor.entry_point), // TODO: Expose the rest of this somehow compilation_options: PipelineCompilationOptions { - constants: &default(), + constants: &[], zero_initialize_workgroup_memory: descriptor .zero_initialize_workgroup_memory, }, @@ -1103,10 +1103,6 @@ fn create_pipeline_task( target_os = "macos", not(feature = "multi_threaded") ))] -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" -)] fn create_pipeline_task( task: impl Future> + Send + 'static, _sync: bool, @@ -1159,8 +1155,12 @@ fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabiliti features.contains(Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING), ); capabilities.set( - Capabilities::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING, - features.contains(Features::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING), + Capabilities::STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING, + features.contains(Features::STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING), + ); + capabilities.set( + Capabilities::UNIFORM_BUFFER_ARRAY_NON_UNIFORM_INDEXING, + features.contains(Features::UNIFORM_BUFFER_BINDING_ARRAYS), ); // TODO: This needs a proper wgpu feature capabilities.set( @@ -1197,6 +1197,10 @@ fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabiliti Capabilities::MULTISAMPLED_SHADING, downlevel.contains(DownlevelFlags::MULTISAMPLED_SHADING), ); + capabilities.set( + Capabilities::RAY_QUERY, + features.contains(Features::EXPERIMENTAL_RAY_QUERY), + ); capabilities.set( Capabilities::DUAL_SOURCE_BLENDING, features.contains(Features::DUAL_SOURCE_BLENDING), @@ -1229,6 +1233,14 @@ fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabiliti Capabilities::TEXTURE_INT64_ATOMIC, features.contains(Features::TEXTURE_INT64_ATOMIC), ); + capabilities.set( + Capabilities::SHADER_FLOAT16, + features.contains(Features::SHADER_F16), + ); + capabilities.set( + Capabilities::RAY_HIT_VERTEX_POSITION, + features.intersects(Features::EXPERIMENTAL_RAY_HIT_VERTEX_RETURN), + ); capabilities } diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index ff8430b951..1be9fd7427 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -301,6 +301,8 @@ impl From<&Source> for naga_oil::compose::ShaderType { naga::ShaderStage::Vertex => naga_oil::compose::ShaderType::GlslVertex, naga::ShaderStage::Fragment => naga_oil::compose::ShaderType::GlslFragment, naga::ShaderStage::Compute => panic!("glsl compute not yet implemented"), + naga::ShaderStage::Task => panic!("task shaders not yet implemented"), + naga::ShaderStage::Mesh => panic!("mesh shaders not yet implemented"), }, #[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] Source::Glsl(_, _) => panic!( diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index f2cfbcd9d0..5df4967757 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -23,7 +23,7 @@ use bevy_platform::time::Instant; use bevy_time::TimeSender; use wgpu::{ Adapter, AdapterInfo, CommandBuffer, CommandEncoder, DeviceType, Instance, Queue, - RequestAdapterOptions, + RequestAdapterOptions, Trace, }; /// Updates the [`RenderGraph`] with all of its nodes and then runs it to render the entire frame. @@ -177,21 +177,15 @@ pub async fn initialize_renderer( // discrete GPUs due to having to transfer data across the PCI-E bus and so it // should not be automatically enabled in this case. It is however beneficial for // integrated GPUs. - features -= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS; + features.remove(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS); } - // RAY_QUERY and RAY_TRACING_ACCELERATION STRUCTURE will sometimes cause DeviceLost failures on platforms - // that report them as supported: - // - features -= wgpu::Features::EXPERIMENTAL_RAY_QUERY; - features -= wgpu::Features::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE; - limits = adapter.limits(); } // Enforce the disabled features if let Some(disabled_features) = options.disabled_features { - features -= disabled_features; + features.remove(disabled_features); } // NOTE: |= is used here to ensure that any explicitly-enabled features are respected. features |= options.features; @@ -240,6 +234,12 @@ pub async fn initialize_renderer( max_uniform_buffers_per_shader_stage: limits .max_uniform_buffers_per_shader_stage .min(constrained_limits.max_uniform_buffers_per_shader_stage), + max_binding_array_elements_per_shader_stage: limits + .max_binding_array_elements_per_shader_stage + .min(constrained_limits.max_binding_array_elements_per_shader_stage), + max_binding_array_sampler_elements_per_shader_stage: limits + .max_binding_array_sampler_elements_per_shader_stage + .min(constrained_limits.max_binding_array_sampler_elements_per_shader_stage), max_uniform_buffer_binding_size: limits .max_uniform_buffer_binding_size .min(constrained_limits.max_uniform_buffer_binding_size), @@ -310,15 +310,14 @@ pub async fn initialize_renderer( } let (device, queue) = adapter - .request_device( - &wgpu::DeviceDescriptor { - label: options.device_label.as_ref().map(AsRef::as_ref), - required_features: features, - required_limits: limits, - memory_hints: options.memory_hints.clone(), - }, - options.trace_path.as_deref(), - ) + .request_device(&wgpu::DeviceDescriptor { + label: options.device_label.as_ref().map(AsRef::as_ref), + required_features: features, + required_limits: limits, + memory_hints: options.memory_hints.clone(), + // See https://github.com/gfx-rs/wgpu/issues/5974 + trace: Trace::Off, + }) .await .unwrap(); let queue = Arc::new(WgpuWrapper::new(queue)); diff --git a/crates/bevy_render/src/renderer/render_device.rs b/crates/bevy_render/src/renderer/render_device.rs index 31f47e5740..b1a20d2ace 100644 --- a/crates/bevy_render/src/renderer/render_device.rs +++ b/crates/bevy_render/src/renderer/render_device.rs @@ -7,7 +7,7 @@ use bevy_ecs::resource::Resource; use bevy_utils::WgpuWrapper; use wgpu::{ util::DeviceExt, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, - BindGroupLayoutEntry, BufferAsyncError, BufferBindingType, MaintainResult, + BindGroupLayoutEntry, BufferAsyncError, BufferBindingType, PollError, PollStatus, }; /// This GPU device is responsible for the creation of most rendering and compute resources. @@ -67,11 +67,14 @@ impl RenderDevice { // This call passes binary data to the backend as-is and can potentially result in a driver crash or bogus behavior. // No attempt is made to ensure that data is valid SPIR-V. unsafe { - self.device - .create_shader_module_spirv(&wgpu::ShaderModuleDescriptorSpirV { - label: desc.label, - source: source.clone(), - }) + self.device.create_shader_module_passthrough( + wgpu::ShaderModuleDescriptorPassthrough::SpirV( + wgpu::ShaderModuleDescriptorSpirV { + label: desc.label, + source: source.clone(), + }, + ), + ) } } // SAFETY: @@ -118,7 +121,7 @@ impl RenderDevice { /// /// no-op on the web, device is automatically polled. #[inline] - pub fn poll(&self, maintain: wgpu::Maintain) -> MaintainResult { + pub fn poll(&self, maintain: wgpu::PollType) -> Result { self.device.poll(maintain) } diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index ab50fc81b1..715bbb35f8 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -2,8 +2,8 @@ use crate::renderer::{ RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue, }; use alloc::borrow::Cow; -use std::path::PathBuf; +use wgpu::DxcShaderModel; pub use wgpu::{ Backends, Dx12Compiler, Features as WgpuFeatures, Gles3MinorVersion, InstanceFlags, Limits as WgpuLimits, MemoryHints, PowerPreference, @@ -53,8 +53,6 @@ pub struct WgpuSettings { pub instance_flags: InstanceFlags, /// This hints to the WGPU device about the preferred memory allocation strategy. pub memory_hints: MemoryHints, - /// The path to pass to wgpu for API call tracing. This only has an effect if wgpu's tracing functionality is enabled. - pub trace_path: Option, } impl Default for WgpuSettings { @@ -114,6 +112,7 @@ impl Default for WgpuSettings { Dx12Compiler::DynamicDxc { dxc_path: String::from(dxc), dxil_path: String::from(dxil), + max_shader_model: DxcShaderModel::V6_7, } } else { Dx12Compiler::Fxc @@ -137,7 +136,6 @@ impl Default for WgpuSettings { gles3_minor_version, instance_flags, memory_hints: MemoryHints::default(), - trace_path: None, } } } diff --git a/crates/bevy_render/src/storage.rs b/crates/bevy_render/src/storage.rs index 0046b4e6ac..6084271fee 100644 --- a/crates/bevy_render/src/storage.rs +++ b/crates/bevy_render/src/storage.rs @@ -116,6 +116,7 @@ impl RenderAsset for GpuShaderStorageBuffer { source_asset: Self::SourceAsset, _: AssetId, render_device: &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { match source_asset.data { Some(data) => { diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index dc7aadafbc..b216b5fd32 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,11 +1,11 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; -use bevy_ecs::lifecycle::{OnAdd, OnRemove}; +use bevy_ecs::lifecycle::{Add, Remove}; use bevy_ecs::{ component::Component, entity::{ContainsEntity, Entity, EntityEquivalent}, - observer::Trigger, + observer::On, query::With, reflect::ReflectComponent, resource::Resource, @@ -94,15 +94,15 @@ impl Plugin for SyncWorldPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); app.add_observer( - |trigger: Trigger, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target().unwrap())); + |trigger: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(trigger.target())); }, ); app.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target().unwrap()) { + if let Ok(e) = query.get(trigger.target()) { pending.push(EntityRecord::Removed(*e)); }; }, @@ -281,7 +281,7 @@ mod render_entities_world_query_impls { archetype::Archetype, component::{ComponentId, Components, Tick}, entity::Entity, - query::{FilteredAccess, QueryData, ReadOnlyQueryData, WorldQuery}, + query::{FilteredAccess, QueryData, ReadOnlyQueryData, ReleaseStateQueryData, WorldQuery}, storage::{Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -299,9 +299,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -314,9 +314,9 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, archetype: &'w Archetype, table: &'w Table, ) { @@ -327,9 +327,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. @@ -364,21 +364,24 @@ mod render_entities_world_query_impls { unsafe impl QueryData for RenderEntity { const IS_READ_ONLY: bool = true; type ReadOnly = RenderEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. let component = - unsafe { <&RenderEntity as QueryData>::fetch(fetch, entity, table_row) }; + unsafe { <&RenderEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } @@ -386,6 +389,12 @@ mod render_entities_world_query_impls { // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for RenderEntity {} + impl ReleaseStateQueryData for RenderEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } + /// SAFETY: defers completely to `&RenderEntity` implementation, /// and then only modifies the output safely. unsafe impl WorldQuery for MainEntity { @@ -399,9 +408,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -414,7 +423,7 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, component_id: &ComponentId, archetype: &'w Archetype, @@ -427,9 +436,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. @@ -464,26 +473,36 @@ mod render_entities_world_query_impls { unsafe impl QueryData for MainEntity { const IS_READ_ONLY: bool = true; type ReadOnly = MainEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. - let component = unsafe { <&MainEntity as QueryData>::fetch(fetch, entity, table_row) }; + let component = + unsafe { <&MainEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for MainEntity {} + + impl ReleaseStateQueryData for MainEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } } #[cfg(test)] @@ -491,8 +510,8 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - lifecycle::{OnAdd, OnRemove}, - observer::Trigger, + lifecycle::{Add, Remove}, + observer::On, query::With, system::{Query, ResMut}, world::World, @@ -513,15 +532,15 @@ mod tests { main_world.init_resource::(); main_world.add_observer( - |trigger: Trigger, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target().unwrap())); + |trigger: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(trigger.target())); }, ); main_world.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target().unwrap()) { + if let Ok(e) = query.get(trigger.target()) { pending.push(EntityRecord::Removed(*e)); }; }, diff --git a/crates/bevy_render/src/texture/gpu_image.rs b/crates/bevy_render/src/texture/gpu_image.rs index 551bd3ee02..1337df5e00 100644 --- a/crates/bevy_render/src/texture/gpu_image.rs +++ b/crates/bevy_render/src/texture/gpu_image.rs @@ -7,6 +7,7 @@ use bevy_asset::AssetId; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_image::{Image, ImageSampler}; use bevy_math::{AspectRatio, UVec2}; +use tracing::warn; use wgpu::{Extent3d, TextureFormat, TextureViewDescriptor}; /// The GPU-representation of an [`Image`]. @@ -44,6 +45,7 @@ impl RenderAsset for GpuImage { image: Self::SourceAsset, _: AssetId, (render_device, render_queue, default_sampler): &mut SystemParamItem, + previous_asset: Option<&Self>, ) -> Result> { let texture = if let Some(ref data) = image.data { render_device.create_texture_with_data( @@ -54,7 +56,38 @@ impl RenderAsset for GpuImage { data, ) } else { - render_device.create_texture(&image.texture_descriptor) + let new_texture = render_device.create_texture(&image.texture_descriptor); + if image.copy_on_resize { + if let Some(previous) = previous_asset { + let mut command_encoder = + render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("copy_image_on_resize"), + }); + let copy_size = Extent3d { + width: image.texture_descriptor.size.width.min(previous.size.width), + height: image + .texture_descriptor + .size + .height + .min(previous.size.height), + depth_or_array_layers: image + .texture_descriptor + .size + .depth_or_array_layers + .min(previous.size.depth_or_array_layers), + }; + + command_encoder.copy_texture_to_texture( + previous.texture.as_image_copy(), + new_texture.as_image_copy(), + copy_size, + ); + render_queue.submit([command_encoder.finish()]); + } else { + warn!("No previous asset to copy from for image: {:?}", image); + } + } + new_texture }; let texture_view = texture.create_view( diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 6c2dc67aae..fe37dd4310 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -4,7 +4,7 @@ mod texture_attachment; mod texture_cache; pub use crate::render_resource::DefaultImageSampler; -#[cfg(feature = "basis-universal")] +#[cfg(feature = "compressed_image_saver")] use bevy_image::CompressedImageSaver; #[cfg(feature = "hdr")] use bevy_image::HdrTextureLoader; @@ -84,7 +84,7 @@ impl Plugin for ImagePlugin { image_assets.insert(&Handle::default(), Image::default()); image_assets.insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent()); - #[cfg(feature = "basis-universal")] + #[cfg(feature = "compressed_image_saver")] if let Some(processor) = app .world() .get_resource::() diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index d25e8e7f49..7596799061 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -192,7 +192,7 @@ impl Msaa { 2 => Msaa::Sample2, 4 => Msaa::Sample4, 8 => Msaa::Sample8, - _ => panic!("Unsupported MSAA sample count: {}", samples), + _ => panic!("Unsupported MSAA sample count: {samples}"), } } } @@ -316,7 +316,7 @@ pub struct ExtractedView { impl ExtractedView { /// Creates a 3D rangefinder for a view pub fn rangefinder3d(&self) -> ViewRangefinder3d { - ViewRangefinder3d::from_world_from_view(&self.world_from_view.compute_matrix()) + ViewRangefinder3d::from_world_from_view(&self.world_from_view.to_matrix()) } } @@ -934,7 +934,7 @@ pub fn prepare_view_uniforms( } let view_from_clip = clip_from_view.inverse(); - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let view_from_world = world_from_view.inverse(); let clip_from_world = if temporal_jitter.is_some() { diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 13b8ac74d4..11abdd8803 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -20,7 +20,7 @@ use smallvec::SmallVec; use super::NoCpuCulling; use crate::{ - camera::{Camera, CameraProjection, Projection}, + camera::{Camera, Projection}, mesh::{Mesh, Mesh3d, MeshAabb}, primitives::{Aabb, Frustum, Sphere}, sync_world::MainEntity, diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 56d591a365..33b76d269d 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -39,7 +39,7 @@ use std::{ use tracing::{error, info, warn}; use wgpu::{CommandEncoder, Extent3d, TextureFormat}; -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ScreenshotCaptured(pub Image); @@ -122,7 +122,7 @@ struct RenderScreenshotsPrepared(EntityHashMap); struct RenderScreenshotsSender(Sender<(Entity, Image)>); /// Saves the captured screenshot to disk at the provided path. -pub fn save_to_disk(path: impl AsRef) -> impl FnMut(Trigger) { +pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On) { let path = path.as_ref().to_owned(); move |trigger| { let img = trigger.event().deref().clone(); diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 8a6fe517fd..48d718b410 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_scene" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides scene functionality for Bevy Engine" homepage = "https://bevy.org" @@ -15,19 +15,20 @@ serialize = [ "uuid/serde", "bevy_ecs/serialize", "bevy_platform/serialize", + "bevy_render?/serialize", ] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev", optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev", optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -35,7 +36,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea serde = { version = "1.0", features = ["derive"], optional = true } uuid = { version = "1.13.1", features = ["v4"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 1d684c9dac..2293beef1e 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -93,7 +93,7 @@ impl Scene { type_registry .get(type_id) .ok_or_else(|| SceneSpawnError::UnregisteredType { - std_type_name: component_info.name().to_string(), + std_type_name: component_info.name(), })?; let reflect_resource = registration.data::().ok_or_else(|| { SceneSpawnError::UnregisteredResource { @@ -133,7 +133,7 @@ impl Scene { let registration = type_registry .get(component_info.type_id().unwrap()) .ok_or_else(|| SceneSpawnError::UnregisteredType { - std_type_name: component_info.name().to_string(), + std_type_name: component_info.name(), })?; let reflect_component = registration.data::().ok_or_else(|| { diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 456cb62225..71cd848751 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -2,7 +2,7 @@ use crate::{DynamicScene, Scene}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::{ entity::{Entity, EntityHashMap}, - event::{Event, EventCursor, Events}, + event::{EntityEvent, Event, EventCursor, Events}, hierarchy::ChildOf, reflect::AppTypeRegistry, resource::Resource, @@ -10,6 +10,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::Reflect; +use bevy_utils::prelude::DebugName; use thiserror::Error; use uuid::Uuid; @@ -22,10 +23,10 @@ use bevy_ecs::{ }; /// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use. /// -/// See also [`Trigger`], [`SceneSpawner::instance_is_ready`]. +/// See also [`On`], [`SceneSpawner::instance_is_ready`]. /// -/// [`Trigger`]: bevy_ecs::observer::Trigger -#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, Reflect)] +/// [`On`]: bevy_ecs::observer::On +#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, EntityEvent, Reflect)] #[reflect(Debug, PartialEq, Clone)] pub struct SceneInstanceReady { /// Instance which has been spawned. @@ -105,7 +106,7 @@ pub enum SceneSpawnError { )] UnregisteredType { /// The [type name](std::any::type_name) for the unregistered type. - std_type_name: String, + std_type_name: DebugName, }, /// Scene contains an unregistered type which has a `TypePath`. #[error( @@ -542,7 +543,7 @@ mod tests { use bevy_ecs::{ component::Component, hierarchy::Children, - observer::Trigger, + observer::On, prelude::ReflectComponent, query::With, system::{Commands, Query, Res, ResMut, RunSystemOnce}, @@ -724,7 +725,7 @@ mod tests { fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Option) { // Add observer app.world_mut().add_observer( - move |trigger: Trigger, + move |trigger: On, scene_spawner: Res, mut trigger_count: ResMut| { assert_eq!( @@ -734,7 +735,7 @@ mod tests { ); assert_eq!( trigger.target(), - scene_entity, + scene_entity.unwrap_or(Entity::PLACEHOLDER), "`SceneInstanceReady` triggered on the wrong parent entity" ); assert!( diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index cb8206d3dd..84badb5850 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -543,7 +543,7 @@ mod tests { where S: Serializer, { - serializer.serialize_str(&format!("{:X}", value)) + serializer.serialize_str(&format!("{value:X}")) } pub fn deserialize<'de, D>(deserializer: D) -> Result diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml new file mode 100644 index 0000000000..ffaca58ba3 --- /dev/null +++ b/crates/bevy_solari/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "bevy_solari" +version = "0.17.0-dev" +edition = "2024" +description = "Provides raytraced lighting for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ + "std", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } + +# other +bytemuck = { version = "1" } +derive_more = { version = "2", default-features = false, features = ["from"] } +tracing = { version = "0.1", default-features = false, features = ["std"] } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_solari/LICENSE-APACHE b/crates/bevy_solari/LICENSE-APACHE new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/crates/bevy_solari/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_solari/LICENSE-MIT b/crates/bevy_solari/LICENSE-MIT new file mode 100644 index 0000000000..9cf106272a --- /dev/null +++ b/crates/bevy_solari/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_solari/README.md b/crates/bevy_solari/README.md new file mode 100644 index 0000000000..089418e60d --- /dev/null +++ b/crates/bevy_solari/README.md @@ -0,0 +1,9 @@ +# Bevy Solari + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Downloads](https://img.shields.io/crates/d/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Docs](https://docs.rs/bevy_solari/badge.svg)](https://docs.rs/bevy_solari/latest/bevy_solari/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) + +![Logo](../../assets/branding/bevy_solari.svg) diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs new file mode 100644 index 0000000000..d5a22e014b --- /dev/null +++ b/crates/bevy_solari/src/lib.rs @@ -0,0 +1,52 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + +//! Provides raytraced lighting. +//! +//! See [`SolariPlugin`] for more info. +//! +//! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/assets/branding/bevy_solari.svg) +pub mod pathtracer; +pub mod realtime; +pub mod scene; + +/// The solari prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + pub use super::SolariPlugin; + pub use crate::realtime::SolariLighting; + pub use crate::scene::RaytracingMesh3d; +} + +use crate::realtime::SolariLightingPlugin; +use crate::scene::RaytracingScenePlugin; +use bevy_app::{App, Plugin}; +use bevy_render::settings::WgpuFeatures; + +/// An experimental plugin for raytraced lighting. +/// +/// This plugin provides: +/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting (indirect lighting not yet implemented). +/// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding. +/// * [`pathtracer::PathtracingPlugin`] - A non-realtime pathtracer for validation purposes. +/// +/// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::` to your entities. +pub struct SolariPlugin; + +impl Plugin for SolariPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((RaytracingScenePlugin, SolariLightingPlugin)); + } +} + +impl SolariPlugin { + /// [`WgpuFeatures`] required for this plugin to function. + pub fn required_wgpu_features() -> WgpuFeatures { + WgpuFeatures::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE + | WgpuFeatures::EXPERIMENTAL_RAY_QUERY + | WgpuFeatures::BUFFER_BINDING_ARRAY + | WgpuFeatures::TEXTURE_BINDING_ARRAY + | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING + | WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY + } +} diff --git a/crates/bevy_solari/src/pathtracer/extract.rs b/crates/bevy_solari/src/pathtracer/extract.rs new file mode 100644 index 0000000000..38f27968a6 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/extract.rs @@ -0,0 +1,33 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer}; +use bevy_ecs::{ + change_detection::DetectChanges, + system::{Commands, Query}, + world::Ref, +}; +use bevy_render::{camera::Camera, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_pathtracer( + cameras_3d: Extract< + Query<( + RenderEntity, + &Camera, + Ref, + Option<&Pathtracer>, + )>, + >, + mut commands: Commands, +) { + for (entity, camera, global_transform, pathtracer) in &cameras_3d { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if pathtracer.is_some() && camera.is_active { + let mut pathtracer = pathtracer.unwrap().clone(); + pathtracer.reset |= global_transform.is_changed(); + entity_commands.insert(pathtracer); + } else { + entity_commands.remove::<(Pathtracer, PathtracerAccumulationTexture)>(); + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/mod.rs b/crates/bevy_solari/src/pathtracer/mod.rs new file mode 100644 index 0000000000..1e2cd95ed8 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/mod.rs @@ -0,0 +1,67 @@ +mod extract; +mod node; +mod prepare; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::embedded_asset; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + render_graph::{RenderGraphApp, ViewNodeRunner}, + renderer::RenderDevice, + view::Hdr, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use extract::extract_pathtracer; +use node::PathtracerNode; +use prepare::prepare_pathtracer_accumulation_texture; +use tracing::warn; + +/// Non-realtime pathtracing. +/// +/// This plugin is meant to generate reference screenshots to compare against, +/// and is not intended to be used by games. +pub struct PathtracingPlugin; + +impl Plugin for PathtracingPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "pathtracer.wgsl"); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "PathtracingPlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + render_app + .add_systems(ExtractSchedule, extract_pathtracer) + .add_systems( + Render, + prepare_pathtracer_accumulation_texture.in_set(RenderSystems::PrepareResources), + ) + .add_render_graph_node::>( + Core3d, + node::graph::PathtracerNode, + ) + .add_render_graph_edges(Core3d, (Node3d::EndMainPass, node::graph::PathtracerNode)); + } +} + +#[derive(Component, Reflect, Default, Clone)] +#[reflect(Component, Default, Clone)] +#[require(Hdr)] +pub struct Pathtracer { + pub reset: bool, +} diff --git a/crates/bevy_solari/src/pathtracer/node.rs b/crates/bevy_solari/src/pathtracer/node.rs new file mode 100644 index 0000000000..30031c1d51 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/node.rs @@ -0,0 +1,134 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer}; +use crate::scene::RaytracingSceneBindings; +use bevy_asset::load_embedded_asset; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{texture_storage_2d, uniform_buffer}, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId, + ComputePassDescriptor, ComputePipelineDescriptor, ImageSubresourceRange, PipelineCache, + ShaderStages, StorageTextureAccess, TextureFormat, + }, + renderer::{RenderContext, RenderDevice}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub struct PathtracerNode; +} + +pub struct PathtracerNode { + bind_group_layout: BindGroupLayout, + pipeline: CachedComputePipelineId, +} + +impl ViewNode for PathtracerNode { + type ViewQuery = ( + &'static Pathtracer, + &'static PathtracerAccumulationTexture, + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (pathtracer, accumulation_texture, camera, view_target, view_uniform_offset): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let (Some(pipeline), Some(scene_bindings), Some(viewport), Some(view_uniforms)) = ( + pipeline_cache.get_compute_pipeline(self.pipeline), + &scene_bindings.bind_group, + camera.physical_viewport_size, + view_uniforms.uniforms.binding(), + ) else { + return Ok(()); + }; + + let bind_group = render_context.render_device().create_bind_group( + "pathtracer_bind_group", + &self.bind_group_layout, + &BindGroupEntries::sequential(( + &accumulation_texture.0.default_view, + view_target.get_unsampled_color_attachment().view, + view_uniforms, + )), + ); + + let command_encoder = render_context.command_encoder(); + + if pathtracer.reset { + command_encoder.clear_texture( + &accumulation_texture.0.texture, + &ImageSubresourceRange::default(), + ); + } + + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("pathtracer"), + timestamp_writes: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, scene_bindings, &[]); + pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + Ok(()) + } +} + +impl FromWorld for PathtracerNode { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + + let bind_group_layout = render_device.create_bind_group_layout( + "pathtracer_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::ReadWrite), + texture_storage_2d( + ViewTarget::TEXTURE_FORMAT_HDR, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(true), + ), + ), + ); + + let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("pathtracer_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![], + shader: load_embedded_asset!(world, "pathtracer.wgsl"), + shader_defs: vec![], + entry_point: "pathtrace".into(), + zero_initialize_workgroup_memory: false, + }); + + Self { + bind_group_layout, + pipeline, + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl new file mode 100644 index 0000000000..c67b53e58e --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -0,0 +1,78 @@ +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::utils::{rand_f, rand_vec2f} +#import bevy_render::maths::PI +#import bevy_render::view::View +#import bevy_solari::sampling::{sample_random_light, sample_cosine_hemisphere} +#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} + +@group(1) @binding(0) var accumulation_texture: texture_storage_2d; +@group(1) @binding(1) var view_output: texture_storage_2d; +@group(1) @binding(2) var view: View; + +@compute @workgroup_size(8, 8, 1) +fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { + return; + } + + let old_color = textureLoad(accumulation_texture, global_id.xy); + + // Setup RNG + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + let frame_index = u32(old_color.a) * 5782582u; + var rng = pixel_index + frame_index; + + // Shoot the first ray from the camera + let pixel_center = vec2(global_id.xy) + 0.5; + let jitter = rand_vec2f(&rng) - 0.5; + let pixel_uv = (pixel_center + jitter) / view.viewport.zw; + let pixel_ndc = (pixel_uv * 2.0) - 1.0; + let primary_ray_target = view.world_from_clip * vec4(pixel_ndc.x, -pixel_ndc.y, 1.0, 1.0); + var ray_origin = view.world_position; + var ray_direction = normalize((primary_ray_target.xyz / primary_ray_target.w) - ray_origin); + var ray_t_min = 0.0; + + // Path trace + var radiance = vec3(0.0); + var throughput = vec3(1.0); + loop { + let ray_hit = trace_ray(ray_origin, ray_direction, ray_t_min, RAY_T_MAX, RAY_FLAG_NONE); + if ray_hit.kind != RAY_QUERY_INTERSECTION_NONE { + let ray_hit = resolve_ray_hit_full(ray_hit); + + // Evaluate material BRDF + let diffuse_brdf = ray_hit.material.base_color / PI; + + // Use emissive only on the first ray (coming from the camera) + if ray_t_min == 0.0 { radiance = ray_hit.material.emissive; } + + // Sample direct lighting + radiance += throughput * diffuse_brdf * sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng); + + // Sample new ray direction from the material BRDF for next bounce + ray_direction = sample_cosine_hemisphere(ray_hit.world_normal, &rng); + + // Update other variables for next bounce + ray_origin = ray_hit.world_position; + ray_t_min = RAY_T_MIN; + + // Update throughput for next bounce + let cos_theta = dot(-ray_direction, ray_hit.world_normal); + let cosine_hemisphere_pdf = cos_theta / PI; // Weight for the next bounce because we importance sampled the diffuse BRDF for the next ray direction + throughput *= (diffuse_brdf * cos_theta) / cosine_hemisphere_pdf; + + // Russian roulette for early termination + let p = luminance(throughput); + if rand_f(&rng) > p { break; } + throughput /= p; + } else { break; } + } + + // Camera exposure + radiance *= view.exposure; + + // Accumulation over time via running average + let new_color = mix(old_color.rgb, radiance, 1.0 / (old_color.a + 1.0)); + textureStore(accumulation_texture, global_id.xy, vec4(new_color, old_color.a + 1.0)); + textureStore(view_output, global_id.xy, vec4(new_color, 1.0)); +} diff --git a/crates/bevy_solari/src/pathtracer/prepare.rs b/crates/bevy_solari/src/pathtracer/prepare.rs new file mode 100644 index 0000000000..7ef4733124 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/prepare.rs @@ -0,0 +1,52 @@ +use super::Pathtracer; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + texture::{CachedTexture, TextureCache}, +}; + +#[derive(Component)] +pub struct PathtracerAccumulationTexture(pub CachedTexture); + +pub fn prepare_pathtracer_accumulation_texture( + query: Query<(Entity, &ExtractedCamera), With>, + mut texture_cache: ResMut, + render_device: Res, + mut commands: Commands, +) { + for (entity, camera) in &query { + let Some(viewport) = camera.physical_viewport_size else { + continue; + }; + + let descriptor = TextureDescriptor { + label: Some("pathtracer_accumulation_texture"), + size: Extent3d { + width: viewport.x, + height: viewport.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::STORAGE_BINDING, + view_formats: &[], + }; + + commands + .entity(entity) + .insert(PathtracerAccumulationTexture( + texture_cache.get(&render_device, descriptor), + )); + } +} diff --git a/crates/bevy_solari/src/realtime/extract.rs b/crates/bevy_solari/src/realtime/extract.rs new file mode 100644 index 0000000000..8e80f02327 --- /dev/null +++ b/crates/bevy_solari/src/realtime/extract.rs @@ -0,0 +1,27 @@ +use super::{prepare::SolariLightingResources, SolariLighting}; +use bevy_ecs::system::{Commands, ResMut}; +use bevy_pbr::deferred::SkipDeferredLighting; +use bevy_render::{camera::Camera, sync_world::RenderEntity, MainWorld}; + +pub fn extract_solari_lighting(mut main_world: ResMut, mut commands: Commands) { + let mut cameras_3d = main_world.query::<(RenderEntity, &Camera, Option<&mut SolariLighting>)>(); + + for (entity, camera, mut solari_lighting) in cameras_3d.iter_mut(&mut main_world) { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if solari_lighting.is_some() && camera.is_active { + entity_commands.insert(( + solari_lighting.as_deref().unwrap().clone(), + SkipDeferredLighting, + )); + solari_lighting.as_mut().unwrap().reset = false; + } else { + entity_commands.remove::<( + SolariLighting, + SolariLightingResources, + SkipDeferredLighting, + )>(); + } + } +} diff --git a/crates/bevy_solari/src/realtime/mod.rs b/crates/bevy_solari/src/realtime/mod.rs new file mode 100644 index 0000000000..9308ab5cf8 --- /dev/null +++ b/crates/bevy_solari/src/realtime/mod.rs @@ -0,0 +1,91 @@ +mod extract; +mod node; +mod prepare; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::embedded_asset; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass}, +}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; +use bevy_pbr::DefaultOpaqueRendererMethod; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + load_shader_library, + render_graph::{RenderGraphApp, ViewNodeRunner}, + renderer::RenderDevice, + view::Hdr, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use extract::extract_solari_lighting; +use node::SolariLightingNode; +use prepare::prepare_solari_lighting_resources; +use tracing::warn; + +pub struct SolariLightingPlugin; + +impl Plugin for SolariLightingPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "restir_di.wgsl"); + load_shader_library!(app, "reservoir.wgsl"); + + app.register_type::() + .insert_resource(DefaultOpaqueRendererMethod::deferred()); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "SolariLightingPlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + render_app + .add_systems(ExtractSchedule, extract_solari_lighting) + .add_systems( + Render, + prepare_solari_lighting_resources.in_set(RenderSystems::PrepareResources), + ) + .add_render_graph_node::>( + Core3d, + node::graph::SolariLightingNode, + ) + .add_render_graph_edges( + Core3d, + (Node3d::EndMainPass, node::graph::SolariLightingNode), + ); + } +} + +/// A component for a 3d camera entity to enable the Solari raytraced lighting system. +/// +/// Must be used with `CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING)`, and +/// `Msaa::Off`. +#[derive(Component, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +#[require(Hdr, DeferredPrepass, DepthPrepass, MotionVectorPrepass)] +pub struct SolariLighting { + /// Set to true to delete the saved temporal history (past frames). + /// + /// Useful for preventing ghosting when the history is no longer + /// representative of the current frame, such as in sudden camera cuts. + /// + /// After setting this to true, it will automatically be toggled + /// back to false at the end of the frame. + pub reset: bool, +} + +impl Default for SolariLighting { + fn default() -> Self { + Self { + reset: true, // No temporal history on the first frame + } + } +} diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs new file mode 100644 index 0000000000..6060bb3c15 --- /dev/null +++ b/crates/bevy_solari/src/realtime/node.rs @@ -0,0 +1,200 @@ +use super::{prepare::SolariLightingResources, SolariLighting}; +use crate::scene::RaytracingSceneBindings; +use bevy_asset::load_embedded_asset; +use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_diagnostic::FrameCount; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{ + storage_buffer_sized, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, + }, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId, + ComputePassDescriptor, ComputePipelineDescriptor, PipelineCache, PushConstantRange, + ShaderStages, StorageTextureAccess, TextureSampleType, + }, + renderer::{RenderContext, RenderDevice}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub struct SolariLightingNode; +} + +pub struct SolariLightingNode { + bind_group_layout: BindGroupLayout, + initial_and_temporal_pipeline: CachedComputePipelineId, + spatial_and_shade_pipeline: CachedComputePipelineId, +} + +impl ViewNode for SolariLightingNode { + type ViewQuery = ( + &'static SolariLighting, + &'static SolariLightingResources, + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + solari_lighting, + solari_lighting_resources, + camera, + view_target, + view_prepass_textures, + view_uniform_offset, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let frame_count = world.resource::(); + let ( + Some(initial_and_temporal_pipeline), + Some(spatial_and_shade_pipeline), + Some(scene_bindings), + Some(viewport), + Some(gbuffer), + Some(depth_buffer), + Some(motion_vectors), + Some(view_uniforms), + ) = ( + pipeline_cache.get_compute_pipeline(self.initial_and_temporal_pipeline), + pipeline_cache.get_compute_pipeline(self.spatial_and_shade_pipeline), + &scene_bindings.bind_group, + camera.physical_viewport_size, + view_prepass_textures.deferred_view(), + view_prepass_textures.depth_view(), + view_prepass_textures.motion_vectors_view(), + view_uniforms.uniforms.binding(), + ) + else { + return Ok(()); + }; + + let bind_group = render_context.render_device().create_bind_group( + "solari_lighting_bind_group", + &self.bind_group_layout, + &BindGroupEntries::sequential(( + view_target.get_unsampled_color_attachment().view, + solari_lighting_resources.reservoirs_a.as_entire_binding(), + solari_lighting_resources.reservoirs_b.as_entire_binding(), + gbuffer, + depth_buffer, + motion_vectors, + view_uniforms, + )), + ); + + // Choice of number here is arbitrary + let frame_index = frame_count.0.wrapping_mul(5782582); + + let diagnostics = render_context.diagnostic_recorder(); + let command_encoder = render_context.command_encoder(); + + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("solari_lighting"), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut pass, "solari_lighting"); + + pass.set_bind_group(0, scene_bindings, &[]); + pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + + pass.set_pipeline(initial_and_temporal_pipeline); + pass.set_push_constants( + 0, + bytemuck::cast_slice(&[frame_index, solari_lighting.reset as u32]), + ); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass.set_pipeline(spatial_and_shade_pipeline); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass_span.end(&mut pass); + + Ok(()) + } +} + +impl FromWorld for SolariLightingNode { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + + let bind_group_layout = render_device.create_bind_group_layout( + "solari_lighting_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d( + ViewTarget::TEXTURE_FORMAT_HDR, + StorageTextureAccess::WriteOnly, + ), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + texture_2d(TextureSampleType::Uint), + texture_depth_2d(), + texture_2d(TextureSampleType::Float { filterable: true }), + uniform_buffer::(true), + ), + ), + ); + + let initial_and_temporal_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_initial_and_temporal_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_di.wgsl"), + shader_defs: vec![], + entry_point: "initial_and_temporal".into(), + zero_initialize_workgroup_memory: false, + }); + + let spatial_and_shade_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_spatial_and_shade_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_di.wgsl"), + shader_defs: vec![], + entry_point: "spatial_and_shade".into(), + zero_initialize_workgroup_memory: false, + }); + + Self { + bind_group_layout, + initial_and_temporal_pipeline, + spatial_and_shade_pipeline, + } + } +} diff --git a/crates/bevy_solari/src/realtime/prepare.rs b/crates/bevy_solari/src/realtime/prepare.rs new file mode 100644 index 0000000000..4f153bf0dc --- /dev/null +++ b/crates/bevy_solari/src/realtime/prepare.rs @@ -0,0 +1,65 @@ +use super::SolariLighting; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res}, +}; +use bevy_math::UVec2; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{Buffer, BufferDescriptor, BufferUsages}, + renderer::RenderDevice, +}; + +/// Size of a Reservoir shader struct in bytes. +const RESERVOIR_STRUCT_SIZE: u64 = 32; + +/// Internal rendering resources used for Solari lighting. +#[derive(Component)] +pub struct SolariLightingResources { + pub reservoirs_a: Buffer, + pub reservoirs_b: Buffer, + pub view_size: UVec2, +} + +pub fn prepare_solari_lighting_resources( + query: Query< + (Entity, &ExtractedCamera, Option<&SolariLightingResources>), + With, + >, + render_device: Res, + mut commands: Commands, +) { + for (entity, camera, solari_lighting_resources) in &query { + let Some(view_size) = camera.physical_viewport_size else { + continue; + }; + + if solari_lighting_resources.map(|r| r.view_size) == Some(view_size) { + continue; + } + + let size = (view_size.x * view_size.y) as u64 * RESERVOIR_STRUCT_SIZE; + + let reservoirs_a = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_reservoirs_a"), + size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + let reservoirs_b = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_reservoirs_b"), + size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + commands.entity(entity).insert(SolariLightingResources { + reservoirs_a, + reservoirs_b, + view_size, + }); + } +} diff --git a/crates/bevy_solari/src/realtime/reservoir.wgsl b/crates/bevy_solari/src/realtime/reservoir.wgsl new file mode 100644 index 0000000000..08a7e26f7c --- /dev/null +++ b/crates/bevy_solari/src/realtime/reservoir.wgsl @@ -0,0 +1,30 @@ +// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf + +#define_import_path bevy_solari::reservoir + +#import bevy_solari::sampling::LightSample + +const NULL_RESERVOIR_SAMPLE = 0xFFFFFFFFu; + +// Don't adjust the size of this struct without also adjusting RESERVOIR_STRUCT_SIZE. +struct Reservoir { + sample: LightSample, + weight_sum: f32, + confidence_weight: f32, + unbiased_contribution_weight: f32, + _padding: f32, +} + +fn empty_reservoir() -> Reservoir { + return Reservoir( + LightSample(vec2(NULL_RESERVOIR_SAMPLE, 0u), vec2(0.0)), + 0.0, + 0.0, + 0.0, + 0.0 + ); +} + +fn reservoir_valid(reservoir: Reservoir) -> bool { + return reservoir.sample.light_id.x != NULL_RESERVOIR_SAMPLE; +} diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl new file mode 100644 index 0000000000..511fd63d12 --- /dev/null +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -0,0 +1,117 @@ +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::rgb9e5::rgb9e5_to_vec3_ +#import bevy_pbr::utils::{rand_f, octahedral_decode} +#import bevy_render::maths::PI +#import bevy_render::view::View +#import bevy_solari::reservoir::{Reservoir, empty_reservoir, reservoir_valid} +#import bevy_solari::sampling::{generate_random_light_sample, calculate_light_contribution, trace_light_visibility} + +@group(1) @binding(0) var view_output: texture_storage_2d; +@group(1) @binding(1) var reservoirs_a: array; +@group(1) @binding(2) var reservoirs_b: array; +@group(1) @binding(3) var gbuffer: texture_2d; +@group(1) @binding(4) var depth_buffer: texture_depth_2d; +@group(1) @binding(5) var motion_vectors: texture_2d; +@group(1) @binding(6) var view: View; +struct PushConstants { frame_index: u32, reset: u32 } +var constants: PushConstants; + +const INITIAL_SAMPLES = 32u; +const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; +const CONFIDENCE_WEIGHT_CAP = 20.0 * f32(INITIAL_SAMPLES); + +@compute @workgroup_size(8, 8, 1) +fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + reservoirs_b[pixel_index] = empty_reservoir(); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; + + let initial_reservoir = generate_initial_reservoir(world_position, world_normal, diffuse_brdf, &rng); + + reservoirs_b[pixel_index] = initial_reservoir; +} + +@compute @workgroup_size(8, 8, 1) +fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + reservoirs_a[pixel_index] = empty_reservoir(); + textureStore(view_output, global_id.xy, vec4(vec3(0.0), 1.0)); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; + let emissive = rgb9e5_to_vec3_(gpixel.g); + + let input_reservoir = reservoirs_b[pixel_index]; + + var radiance = vec3(0.0); + if reservoir_valid(input_reservoir) { + radiance = calculate_light_contribution(input_reservoir.sample, world_position, world_normal).radiance; + } + + reservoirs_a[pixel_index] = input_reservoir; + + var pixel_color = radiance * input_reservoir.unbiased_contribution_weight; + pixel_color *= view.exposure; + pixel_color *= diffuse_brdf; + pixel_color += emissive; + textureStore(view_output, global_id.xy, vec4(pixel_color, 1.0)); +} + +fn generate_initial_reservoir(world_position: vec3, world_normal: vec3, diffuse_brdf: vec3, rng: ptr) -> Reservoir{ + var reservoir = empty_reservoir(); + var reservoir_target_function = 0.0; + for (var i = 0u; i < INITIAL_SAMPLES; i++) { + let light_sample = generate_random_light_sample(rng); + + let mis_weight = 1.0 / f32(INITIAL_SAMPLES); + let light_contribution = calculate_light_contribution(light_sample, world_position, world_normal); + let target_function = luminance(light_contribution.radiance * diffuse_brdf); + let resampling_weight = mis_weight * (target_function * light_contribution.inverse_pdf); + + reservoir.weight_sum += resampling_weight; + + if rand_f(rng) < resampling_weight / reservoir.weight_sum { + reservoir.sample = light_sample; + reservoir_target_function = target_function; + } + } + + if reservoir_valid(reservoir) { + let inverse_target_function = select(0.0, 1.0 / reservoir_target_function, reservoir_target_function > 0.0); + reservoir.unbiased_contribution_weight = reservoir.weight_sum * inverse_target_function; + reservoir.unbiased_contribution_weight *= trace_light_visibility(reservoir.sample, world_position); + } + + reservoir.confidence_weight = f32(INITIAL_SAMPLES); + return reservoir; +} + +fn reconstruct_world_position(pixel_id: vec2, depth: f32) -> vec3 { + let uv = (vec2(pixel_id) + 0.5) / view.viewport.zw; + let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0); + let world_pos = view.world_from_clip * vec4(xy_ndc, depth, 1.0); + return world_pos.xyz / world_pos.w; +} diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs new file mode 100644 index 0000000000..889efb538e --- /dev/null +++ b/crates/bevy_solari/src/scene/binder.rs @@ -0,0 +1,366 @@ +use super::{blas::BlasManager, extract::StandardMaterialAssets, RaytracingMesh3d}; +use bevy_asset::{AssetId, Handle}; +use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_ecs::{ + resource::Resource, + system::{Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_math::{ops::cos, Mat4, Vec3}; +use bevy_pbr::{ExtractedDirectionalLight, MeshMaterial3d, StandardMaterial}; +use bevy_platform::{collections::HashMap, hash::FixedHasher}; +use bevy_render::{ + mesh::allocator::MeshAllocator, + render_asset::RenderAssets, + render_resource::{binding_types::*, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{FallbackImage, GpuImage}, +}; +use bevy_transform::components::GlobalTransform; +use core::{f32::consts::TAU, hash::Hash, num::NonZeroU32, ops::Deref}; + +const MAX_MESH_SLAB_COUNT: NonZeroU32 = NonZeroU32::new(500).unwrap(); +const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap(); + +/// Average angular diameter of the sun as seen from earth. +/// +const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842; + +#[derive(Resource)] +pub struct RaytracingSceneBindings { + pub bind_group: Option, + pub bind_group_layout: BindGroupLayout, +} + +pub fn prepare_raytracing_scene_bindings( + instances_query: Query<( + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + directional_lights_query: Query<&ExtractedDirectionalLight>, + mesh_allocator: Res, + blas_manager: Res, + material_assets: Res, + texture_assets: Res>, + fallback_texture: Res, + render_device: Res, + render_queue: Res, + mut raytracing_scene_bindings: ResMut, +) { + raytracing_scene_bindings.bind_group = None; + + if instances_query.iter().len() == 0 { + return; + } + + let mut vertex_buffers = CachedBindingArray::new(); + let mut index_buffers = CachedBindingArray::new(); + let mut textures = CachedBindingArray::new(); + let mut samplers = Vec::new(); + let mut materials = StorageBufferList::::default(); + let mut tlas = TlasPackage::new(render_device.wgpu_device().create_tlas( + &CreateTlasDescriptor { + label: Some("tlas"), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + max_instances: instances_query.iter().len() as u32, + }, + )); + let mut transforms = StorageBufferList::::default(); + let mut geometry_ids = StorageBufferList::::default(); + let mut material_ids = StorageBufferList::::default(); + let mut light_sources = StorageBufferList::::default(); + let mut directional_lights = StorageBufferList::::default(); + + let mut material_id_map: HashMap, u32, FixedHasher> = + HashMap::default(); + let mut material_id = 0; + let mut process_texture = |texture_handle: &Option>| -> Option { + match texture_handle { + Some(texture_handle) => match texture_assets.get(texture_handle.id()) { + Some(texture) => { + let (texture_id, is_new) = + textures.push_if_absent(texture.texture_view.deref(), texture_handle.id()); + if is_new { + samplers.push(texture.sampler.deref()); + } + Some(texture_id) + } + None => None, + }, + None => Some(u32::MAX), + } + }; + for (asset_id, material) in material_assets.iter() { + let Some(base_color_texture_id) = process_texture(&material.base_color_texture) else { + continue; + }; + let Some(normal_map_texture_id) = process_texture(&material.normal_map_texture) else { + continue; + }; + let Some(emissive_texture_id) = process_texture(&material.emissive_texture) else { + continue; + }; + + materials.get_mut().push(GpuMaterial { + base_color: material.base_color.to_linear(), + emissive: material.emissive, + base_color_texture_id, + normal_map_texture_id, + emissive_texture_id, + _padding: Default::default(), + }); + + material_id_map.insert(*asset_id, material_id); + material_id += 1; + } + + if material_id == 0 { + return; + } + + if textures.is_empty() { + textures.vec.push(fallback_texture.d2.texture_view.deref()); + samplers.push(fallback_texture.d2.sampler.deref()); + } + + let mut instance_id = 0; + for (mesh, material, transform) in &instances_query { + let Some(blas) = blas_manager.get(&mesh.id()) else { + continue; + }; + let Some(vertex_slice) = mesh_allocator.mesh_vertex_slice(&mesh.id()) else { + continue; + }; + let Some(index_slice) = mesh_allocator.mesh_index_slice(&mesh.id()) else { + continue; + }; + let Some(material_id) = material_id_map.get(&material.id()).copied() else { + continue; + }; + let Some(material) = materials.get().get(material_id as usize) else { + continue; + }; + + let transform = transform.to_matrix(); + *tlas.get_mut_single(instance_id).unwrap() = Some(TlasInstance::new( + blas, + tlas_transform(&transform), + Default::default(), + 0xFF, + )); + + transforms.get_mut().push(transform); + + let (vertex_buffer_id, _) = vertex_buffers.push_if_absent( + vertex_slice.buffer.as_entire_buffer_binding(), + vertex_slice.buffer.id(), + ); + let (index_buffer_id, _) = index_buffers.push_if_absent( + index_slice.buffer.as_entire_buffer_binding(), + index_slice.buffer.id(), + ); + + geometry_ids.get_mut().push(GpuInstanceGeometryIds { + vertex_buffer_id, + vertex_buffer_offset: vertex_slice.range.start, + index_buffer_id, + index_buffer_offset: index_slice.range.start, + }); + + material_ids.get_mut().push(material_id); + + if material.emissive != LinearRgba::BLACK { + light_sources + .get_mut() + .push(GpuLightSource::new_emissive_mesh_light( + instance_id as u32, + (index_slice.range.len() / 3) as u32, + )); + } + + instance_id += 1; + } + + if instance_id == 0 { + return; + } + + for directional_light in &directional_lights_query { + let directional_lights = directional_lights.get_mut(); + let directional_light_id = directional_lights.len() as u32; + + directional_lights.push(GpuDirectionalLight::new(directional_light)); + + light_sources + .get_mut() + .push(GpuLightSource::new_directional_light(directional_light_id)); + } + + materials.write_buffer(&render_device, &render_queue); + transforms.write_buffer(&render_device, &render_queue); + geometry_ids.write_buffer(&render_device, &render_queue); + material_ids.write_buffer(&render_device, &render_queue); + light_sources.write_buffer(&render_device, &render_queue); + directional_lights.write_buffer(&render_device, &render_queue); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_tlas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&[], [&tlas]); + render_queue.submit([command_encoder.finish()]); + + raytracing_scene_bindings.bind_group = Some(render_device.create_bind_group( + "raytracing_scene_bind_group", + &raytracing_scene_bindings.bind_group_layout, + &BindGroupEntries::sequential(( + vertex_buffers.as_slice(), + index_buffers.as_slice(), + textures.as_slice(), + samplers.as_slice(), + materials.binding().unwrap(), + tlas.as_binding(), + transforms.binding().unwrap(), + geometry_ids.binding().unwrap(), + material_ids.binding().unwrap(), + light_sources.binding().unwrap(), + directional_lights.binding().unwrap(), + )), + )); +} + +impl FromWorld for RaytracingSceneBindings { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + Self { + bind_group: None, + bind_group_layout: render_device.create_bind_group_layout( + "raytracing_scene_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + texture_2d(TextureSampleType::Float { filterable: true }) + .count(MAX_TEXTURE_COUNT), + sampler(SamplerBindingType::Filtering).count(MAX_TEXTURE_COUNT), + storage_buffer_read_only_sized(false, None), + acceleration_structure(), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + } + } +} + +struct CachedBindingArray { + map: HashMap, + vec: Vec, +} + +impl CachedBindingArray { + fn new() -> Self { + Self { + map: HashMap::default(), + vec: Vec::default(), + } + } + + fn push_if_absent(&mut self, item: T, item_id: I) -> (u32, bool) { + let mut is_new = false; + let i = *self.map.entry(item_id).or_insert_with(|| { + is_new = true; + let i = self.vec.len() as u32; + self.vec.push(item); + i + }); + (i, is_new) + } + + fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + fn as_slice(&self) -> &[T] { + self.vec.as_slice() + } +} + +type StorageBufferList = StorageBuffer>; + +#[derive(ShaderType)] +struct GpuInstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +#[derive(ShaderType)] +struct GpuMaterial { + base_color: LinearRgba, + emissive: LinearRgba, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +#[derive(ShaderType)] +struct GpuLightSource { + kind: u32, + id: u32, +} + +impl GpuLightSource { + fn new_emissive_mesh_light(instance_id: u32, triangle_count: u32) -> GpuLightSource { + Self { + kind: triangle_count << 1, + id: instance_id, + } + } + + fn new_directional_light(directional_light_id: u32) -> GpuLightSource { + Self { + kind: 1, + id: directional_light_id, + } + } +} + +#[derive(ShaderType, Default)] +struct GpuDirectionalLight { + direction_to_light: Vec3, + cos_theta_max: f32, + luminance: Vec3, + inverse_pdf: f32, +} + +impl GpuDirectionalLight { + fn new(directional_light: &ExtractedDirectionalLight) -> Self { + let cos_theta_max = cos(SUN_ANGULAR_DIAMETER_RADIANS / 2.0); + let solid_angle = TAU * (1.0 - cos_theta_max); + let luminance = + (directional_light.color.to_vec3() * directional_light.illuminance) / solid_angle; + + Self { + direction_to_light: directional_light.transform.back().into(), + cos_theta_max, + luminance, + inverse_pdf: solid_angle, + } + } +} + +fn tlas_transform(transform: &Mat4) -> [f32; 12] { + transform.transpose().to_cols_array()[..12] + .try_into() + .unwrap() +} diff --git a/crates/bevy_solari/src/scene/blas.rs b/crates/bevy_solari/src/scene/blas.rs new file mode 100644 index 0000000000..5beaa3b57c --- /dev/null +++ b/crates/bevy_solari/src/scene/blas.rs @@ -0,0 +1,133 @@ +use bevy_asset::AssetId; +use bevy_ecs::{ + resource::Resource, + system::{Res, ResMut}, +}; +use bevy_mesh::{Indices, Mesh}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + mesh::{ + allocator::{MeshAllocator, MeshBufferSlice}, + RenderMesh, + }, + render_asset::ExtractedAssets, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, +}; + +#[derive(Resource, Default)] +pub struct BlasManager(HashMap, Blas>); + +impl BlasManager { + pub fn get(&self, mesh: &AssetId) -> Option<&Blas> { + self.0.get(mesh) + } +} + +pub fn prepare_raytracing_blas( + mut blas_manager: ResMut, + extracted_meshes: Res>, + mesh_allocator: Res, + render_device: Res, + render_queue: Res, +) { + let blas_manager = &mut blas_manager.0; + + // Delete BLAS for deleted or modified meshes + for asset_id in extracted_meshes + .removed + .iter() + .chain(extracted_meshes.modified.iter()) + { + blas_manager.remove(asset_id); + } + + if extracted_meshes.extracted.is_empty() { + return; + } + + // Create new BLAS for added or changed meshes + let blas_resources = extracted_meshes + .extracted + .iter() + .filter(|(_, mesh)| is_mesh_raytracing_compatible(mesh)) + .map(|(asset_id, _)| { + let vertex_slice = mesh_allocator.mesh_vertex_slice(asset_id).unwrap(); + let index_slice = mesh_allocator.mesh_index_slice(asset_id).unwrap(); + + let (blas, blas_size) = + allocate_blas(&vertex_slice, &index_slice, asset_id, &render_device); + + blas_manager.insert(*asset_id, blas); + + (*asset_id, vertex_slice, index_slice, blas_size) + }) + .collect::>(); + + // Build geometry into each BLAS + let build_entries = blas_resources + .iter() + .map(|(asset_id, vertex_slice, index_slice, blas_size)| { + let geometry = BlasTriangleGeometry { + size: blas_size, + vertex_buffer: vertex_slice.buffer, + first_vertex: vertex_slice.range.start, + vertex_stride: 48, + index_buffer: Some(index_slice.buffer), + first_index: Some(index_slice.range.start), + transform_buffer: None, + transform_buffer_offset: None, + }; + BlasBuildEntry { + blas: &blas_manager[asset_id], + geometry: BlasGeometries::TriangleGeometries(vec![geometry]), + } + }) + .collect::>(); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_blas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&build_entries, &[]); + render_queue.submit([command_encoder.finish()]); +} + +fn allocate_blas( + vertex_slice: &MeshBufferSlice, + index_slice: &MeshBufferSlice, + asset_id: &AssetId, + render_device: &RenderDevice, +) -> (Blas, BlasTriangleGeometrySizeDescriptor) { + let blas_size = BlasTriangleGeometrySizeDescriptor { + vertex_format: Mesh::ATTRIBUTE_POSITION.format, + vertex_count: vertex_slice.range.len() as u32, + index_format: Some(IndexFormat::Uint32), + index_count: Some(index_slice.range.len() as u32), + flags: AccelerationStructureGeometryFlags::OPAQUE, + }; + + let blas = render_device.wgpu_device().create_blas( + &CreateBlasDescriptor { + label: Some(&asset_id.to_string()), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + }, + BlasGeometrySizeDescriptors::Triangles { + descriptors: vec![blas_size.clone()], + }, + ); + + (blas, blas_size) +} + +fn is_mesh_raytracing_compatible(mesh: &Mesh) -> bool { + let triangle_list = mesh.primitive_topology() == PrimitiveTopology::TriangleList; + let vertex_attributes = mesh.attributes().map(|(attribute, _)| attribute.id).eq([ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ]); + let indexed_32 = matches!(mesh.indices(), Some(Indices::U32(..))); + mesh.enable_raytracing && triangle_list && vertex_attributes && indexed_32 +} diff --git a/crates/bevy_solari/src/scene/extract.rs b/crates/bevy_solari/src/scene/extract.rs new file mode 100644 index 0000000000..46b11ba2b4 --- /dev/null +++ b/crates/bevy_solari/src/scene/extract.rs @@ -0,0 +1,45 @@ +use super::RaytracingMesh3d; +use bevy_asset::{AssetId, Assets}; +use bevy_derive::Deref; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Query}, +}; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_platform::collections::HashMap; +use bevy_render::{extract_resource::ExtractResource, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_raytracing_scene( + instances: Extract< + Query<( + RenderEntity, + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + >, + mut commands: Commands, +) { + for (render_entity, mesh, material, transform) in &instances { + commands + .entity(render_entity) + .insert((mesh.clone(), material.clone(), *transform)); + } +} + +#[derive(Resource, Deref, Default)] +pub struct StandardMaterialAssets(HashMap, StandardMaterial>); + +impl ExtractResource for StandardMaterialAssets { + type Source = Assets; + + fn extract_resource(source: &Self::Source) -> Self { + Self( + source + .iter() + .map(|(asset_id, material)| (asset_id, material.clone())) + .collect(), + ) + } +} diff --git a/crates/bevy_solari/src/scene/mod.rs b/crates/bevy_solari/src/scene/mod.rs new file mode 100644 index 0000000000..a68e126480 --- /dev/null +++ b/crates/bevy_solari/src/scene/mod.rs @@ -0,0 +1,77 @@ +mod binder; +mod blas; +mod extract; +mod types; + +pub use binder::RaytracingSceneBindings; +pub use types::RaytracingMesh3d; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_ecs::schedule::IntoScheduleConfigs; +use bevy_render::{ + extract_resource::ExtractResourcePlugin, + load_shader_library, + mesh::{ + allocator::{allocate_and_free_meshes, MeshAllocator}, + RenderMesh, + }, + render_asset::prepare_assets, + render_resource::BufferUsages, + renderer::RenderDevice, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use binder::prepare_raytracing_scene_bindings; +use blas::{prepare_raytracing_blas, BlasManager}; +use extract::{extract_raytracing_scene, StandardMaterialAssets}; +use tracing::warn; + +/// Creates acceleration structures and binding arrays of resources for raytracing. +pub struct RaytracingScenePlugin; + +impl Plugin for RaytracingScenePlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "raytracing_scene_bindings.wgsl"); + load_shader_library!(app, "sampling.wgsl"); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "RaytracingScenePlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + app.add_plugins(ExtractResourcePlugin::::default()); + + let render_app = app.sub_app_mut(RenderApp); + + render_app + .world_mut() + .resource_mut::() + .extra_buffer_usages |= BufferUsages::BLAS_INPUT | BufferUsages::STORAGE; + + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, extract_raytracing_scene) + .add_systems( + Render, + ( + prepare_raytracing_blas + .in_set(RenderSystems::PrepareAssets) + .before(prepare_assets::) + .after(allocate_and_free_meshes), + prepare_raytracing_scene_bindings.in_set(RenderSystems::PrepareBindGroups), + ), + ); + } +} diff --git a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl new file mode 100644 index 0000000000..974ee50d7d --- /dev/null +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -0,0 +1,164 @@ +#define_import_path bevy_solari::scene_bindings + +struct InstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +struct VertexBuffer { vertices: array } + +struct IndexBuffer { indices: array } + +struct PackedVertex { + a: vec4, + b: vec4, + tangent: vec4, +} + +struct Vertex { + position: vec3, + normal: vec3, + uv: vec2, + tangent: vec4, +} + +fn unpack_vertex(packed: PackedVertex) -> Vertex { + var vertex: Vertex; + vertex.position = packed.a.xyz; + vertex.normal = vec3(packed.a.w, packed.b.xy); + vertex.uv = packed.b.zw; + vertex.tangent = packed.tangent; + return vertex; +} + +struct Material { + base_color: vec4, + emissive: vec4, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +const TEXTURE_MAP_NONE = 0xFFFFFFFFu; + +struct LightSource { + kind: u32, // 1 bit for kind, 31 bits for extra data + id: u32, +} + +const LIGHT_SOURCE_KIND_EMISSIVE_MESH = 0u; +const LIGHT_SOURCE_KIND_DIRECTIONAL = 1u; + +struct DirectionalLight { + direction_to_light: vec3, + cos_theta_max: f32, + luminance: vec3, + inverse_pdf: f32, +} + +@group(0) @binding(0) var vertex_buffers: binding_array; +@group(0) @binding(1) var index_buffers: binding_array; +@group(0) @binding(2) var textures: binding_array>; +@group(0) @binding(3) var samplers: binding_array; +@group(0) @binding(4) var materials: array; +@group(0) @binding(5) var tlas: acceleration_structure; +@group(0) @binding(6) var transforms: array>; +@group(0) @binding(7) var geometry_ids: array; +@group(0) @binding(8) var material_ids: array; // TODO: Store material_id in instance_custom_index instead? +@group(0) @binding(9) var light_sources: array; +@group(0) @binding(10) var directional_lights: array; + +const RAY_T_MIN = 0.01f; +const RAY_T_MAX = 100000.0f; + +const RAY_NO_CULL = 0xFFu; + +fn trace_ray(ray_origin: vec3, ray_direction: vec3, ray_t_min: f32, ray_t_max: f32, ray_flag: u32) -> RayIntersection { + let ray = RayDesc(ray_flag, RAY_NO_CULL, ray_t_min, ray_t_max, ray_origin, ray_direction); + var rq: ray_query; + rayQueryInitialize(&rq, tlas, ray); + rayQueryProceed(&rq); + return rayQueryGetCommittedIntersection(&rq); +} + +fn sample_texture(id: u32, uv: vec2) -> vec3 { + return textureSampleLevel(textures[id], samplers[id], uv, 0.0).rgb; // TODO: Mipmap +} + +struct ResolvedMaterial { + base_color: vec3, + emissive: vec3, +} + +struct ResolvedRayHitFull { + world_position: vec3, + world_normal: vec3, + geometric_world_normal: vec3, + uv: vec2, + triangle_area: f32, + material: ResolvedMaterial, +} + +fn resolve_material(material: Material, uv: vec2) -> ResolvedMaterial { + var m: ResolvedMaterial; + + m.base_color = material.base_color.rgb; + if material.base_color_texture_id != TEXTURE_MAP_NONE { + m.base_color *= sample_texture(material.base_color_texture_id, uv); + } + + m.emissive = material.emissive.rgb; + if material.emissive_texture_id != TEXTURE_MAP_NONE { + m.emissive *= sample_texture(material.emissive_texture_id, uv); + } + + return m; +} + +fn resolve_ray_hit_full(ray_hit: RayIntersection) -> ResolvedRayHitFull { + let barycentrics = vec3(1.0 - ray_hit.barycentrics.x - ray_hit.barycentrics.y, ray_hit.barycentrics); + return resolve_triangle_data_full(ray_hit.instance_index, ray_hit.primitive_index, barycentrics); +} + +fn resolve_triangle_data_full(instance_id: u32, triangle_id: u32, barycentrics: vec3) -> ResolvedRayHitFull { + let instance_geometry_ids = geometry_ids[instance_id]; + let material_id = material_ids[instance_id]; + + let index_buffer = &index_buffers[instance_geometry_ids.index_buffer_id].indices; + let vertex_buffer = &vertex_buffers[instance_geometry_ids.vertex_buffer_id].vertices; + let material = materials[material_id]; + + let indices_i = (triangle_id * 3u) + vec3(0u, 1u, 2u) + instance_geometry_ids.index_buffer_offset; + let indices = vec3((*index_buffer)[indices_i.x], (*index_buffer)[indices_i.y], (*index_buffer)[indices_i.z]) + instance_geometry_ids.vertex_buffer_offset; + let vertices = array(unpack_vertex((*vertex_buffer)[indices.x]), unpack_vertex((*vertex_buffer)[indices.y]), unpack_vertex((*vertex_buffer)[indices.z])); + + let transform = transforms[instance_id]; + let local_position = mat3x3(vertices[0].position, vertices[1].position, vertices[2].position) * barycentrics; + let world_position = (transform * vec4(local_position, 1.0)).xyz; + + let uv = mat3x2(vertices[0].uv, vertices[1].uv, vertices[2].uv) * barycentrics; + + let local_normal = mat3x3(vertices[0].normal, vertices[1].normal, vertices[2].normal) * barycentrics; // TODO: Use barycentric lerp, ray_hit.object_to_world, cross product geo normal + var world_normal = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_normal); + let geometric_world_normal = world_normal; + if material.normal_map_texture_id != TEXTURE_MAP_NONE { + let local_tangent = mat3x3(vertices[0].tangent.xyz, vertices[1].tangent.xyz, vertices[2].tangent.xyz) * barycentrics; + let world_tangent = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_tangent); + let N = world_normal; + let T = world_tangent; + let B = vertices[0].tangent.w * cross(N, T); + let Nt = sample_texture(material.normal_map_texture_id, uv); + world_normal = normalize(Nt.x * T + Nt.y * B + Nt.z * N); + } + + let triangle_edge0 = vertices[0].position - vertices[1].position; + let triangle_edge1 = vertices[0].position - vertices[2].position; + let triangle_area = length(cross(triangle_edge0, triangle_edge1)) / 2.0; + + let resolved_material = resolve_material(material, uv); + + return ResolvedRayHitFull(world_position, world_normal, geometric_world_normal, uv, triangle_area, resolved_material); +} diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl new file mode 100644 index 0000000000..06142192b6 --- /dev/null +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -0,0 +1,177 @@ +#define_import_path bevy_solari::sampling + +#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u} +#import bevy_render::maths::PI_2 +#import bevy_solari::scene_bindings::{trace_ray, RAY_T_MIN, RAY_T_MAX, light_sources, directional_lights, LIGHT_SOURCE_KIND_DIRECTIONAL, resolve_triangle_data_full} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec28%3A303 +fn sample_cosine_hemisphere(normal: vec3, rng: ptr) -> vec3 { + let cos_theta = 1.0 - 2.0 * rand_f(rng); + let phi = PI_2 * rand_f(rng); + let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0)); + let x = normal.x + sin_theta * cos(phi); + let y = normal.y + sin_theta * sin(phi); + let z = normal.z + cos_theta; + return vec3(x, y, z); +} + +fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rng: ptr) -> vec3 { + let light_sample = generate_random_light_sample(rng); + let light_contribution = calculate_light_contribution(light_sample, ray_origin, origin_world_normal); + let visibility = trace_light_visibility(light_sample, ray_origin); + return light_contribution.radiance * visibility * light_contribution.inverse_pdf; +} + +struct LightSample { + light_id: vec2, + random: vec2, +} + +struct LightContribution { + radiance: vec3, + inverse_pdf: f32, +} + +fn generate_random_light_sample(rng: ptr) -> LightSample { + let light_count = arrayLength(&light_sources); + let light_id = rand_range_u(light_count, rng); + let random = rand_vec2f(rng); + + let light_source = light_sources[light_id]; + var triangle_id = 0u; + + if light_source.kind != LIGHT_SOURCE_KIND_DIRECTIONAL { + let triangle_count = light_source.kind >> 1u; + triangle_id = rand_range_u(triangle_count, rng); + } + + return LightSample(vec2(light_id, triangle_id), random); +} + +fn calculate_light_contribution(light_sample: LightSample, ray_origin: vec3, origin_world_normal: vec3) -> LightContribution { + let light_id = light_sample.light_id.x; + let light_source = light_sources[light_id]; + + var light_contribution: LightContribution; + if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL { + light_contribution = calculate_directional_light_contribution(light_sample, light_source.id, origin_world_normal); + } else { + let triangle_count = light_source.kind >> 1u; + light_contribution = calculate_emissive_mesh_contribution(light_sample, light_source.id, triangle_count, ray_origin, origin_world_normal); + } + + let light_count = arrayLength(&light_sources); + light_contribution.inverse_pdf *= f32(light_count); + + return light_contribution; +} + +fn calculate_directional_light_contribution(light_sample: LightSample, directional_light_id: u32, origin_world_normal: vec3) -> LightContribution { + let directional_light = directional_lights[directional_light_id]; + +#ifdef DIRECTIONAL_LIGHT_SOFT_SHADOWS + // Sample a random direction within a cone whose base is the sun approximated as a disk + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 + let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = light_sample.random.y * PI_2; + let x = cos(phi) * sin_theta; + let y = sin(phi) * sin_theta; + var ray_direction = vec3(x, y, cos_theta); + + // Rotate the ray so that the cone it was sampled from is aligned with the light direction + ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; +#else + let ray_direction = directional_light.direction_to_light; +#endif + + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + let radiance = directional_light.luminance * cos_theta_origin; + + return LightContribution(radiance, directional_light.inverse_pdf); +} + +fn calculate_emissive_mesh_contribution(light_sample: LightSample, instance_id: u32, triangle_count: u32, ray_origin: vec3, origin_world_normal: vec3) -> LightContribution { + let barycentrics = triangle_barycentrics(light_sample.random); + let triangle_id = light_sample.light_id.y; + + let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); + + let light_distance = distance(ray_origin, triangle_data.world_position); + let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + let cos_theta_light = saturate(dot(-ray_direction, triangle_data.world_normal)); + let light_distance_squared = light_distance * light_distance; + + let radiance = triangle_data.material.emissive.rgb * cos_theta_origin * (cos_theta_light / light_distance_squared); + let inverse_pdf = f32(triangle_count) * triangle_data.triangle_area; + + return LightContribution(radiance, inverse_pdf); +} + +fn trace_light_visibility(light_sample: LightSample, ray_origin: vec3) -> f32 { + let light_id = light_sample.light_id.x; + let light_source = light_sources[light_id]; + + if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL { + return trace_directional_light_visibility(light_sample, light_source.id, ray_origin); + } else { + return trace_emissive_mesh_visibility(light_sample, light_source.id, ray_origin); + } +} + +fn trace_directional_light_visibility(light_sample: LightSample, directional_light_id: u32, ray_origin: vec3) -> f32 { + let directional_light = directional_lights[directional_light_id]; + +#ifdef DIRECTIONAL_LIGHT_SOFT_SHADOWS + // Sample a random direction within a cone whose base is the sun approximated as a disk + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 + let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = light_sample.random.y * PI_2; + let x = cos(phi) * sin_theta; + let y = sin(phi) * sin_theta; + var ray_direction = vec3(x, y, cos_theta); + + // Rotate the ray so that the cone it was sampled from is aligned with the light direction + ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; +#else + let ray_direction = directional_light.direction_to_light; +#endif + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); +} + +fn trace_emissive_mesh_visibility(light_sample: LightSample, instance_id: u32, ray_origin: vec3) -> f32 { + let barycentrics = triangle_barycentrics(light_sample.random); + let triangle_id = light_sample.light_id.y; + + let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); + + let light_distance = distance(ray_origin, triangle_data.world_position); + let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + + let ray_t_max = light_distance - RAY_T_MIN - RAY_T_MIN; + if ray_t_max < RAY_T_MIN { return 0.0; } + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, ray_t_max, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); +} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec22%3A297 +fn triangle_barycentrics(random: vec2) -> vec3 { + var barycentrics = random; + if barycentrics.x + barycentrics.y > 1.0 { barycentrics = 1.0 - barycentrics; } + return vec3(1.0 - barycentrics.x - barycentrics.y, barycentrics); +} + +// https://jcgt.org/published/0006/01/01/paper.pdf +fn build_orthonormal_basis(normal: vec3) -> mat3x3 { + let sign = select(-1.0, 1.0, normal.z >= 0.0); + let a = -1.0 / (sign + normal.z); + let b = normal.x * normal.y * a; + let tangent = vec3(1.0 + sign * normal.x * normal.x * a, sign * b, -sign * normal.x); + let bitangent = vec3(b, sign + normal.y * normal.y * a, -normal.y); + return mat3x3(tangent, bitangent, normal); +} diff --git a/crates/bevy_solari/src/scene/types.rs b/crates/bevy_solari/src/scene/types.rs new file mode 100644 index 0000000000..8ee33b31fc --- /dev/null +++ b/crates/bevy_solari/src/scene/types.rs @@ -0,0 +1,21 @@ +use bevy_asset::Handle; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, prelude::ReflectComponent}; +use bevy_mesh::Mesh; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::sync_world::SyncToRenderWorld; +use bevy_transform::components::Transform; +use derive_more::derive::From; + +/// A mesh component used for raytracing. +/// +/// The mesh used in this component must have [`bevy_render::mesh::Mesh::enable_raytracing`] set to true, +/// use the following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}`, use [`bevy_render::render_resource::PrimitiveTopology::TriangleList`], +/// and use [`bevy_mesh::Indices::U32`]. +/// +/// The material used for this entity must be [`MeshMaterial3d`]. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[reflect(Component, Default, Clone, PartialEq)] +#[require(MeshMaterial3d, Transform, SyncToRenderWorld)] +pub struct RaytracingMesh3d(pub Handle); diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 99d526b1e8..1538ac1ebc 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_sprite" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides sprite functionality for Bevy Engine" homepage = "https://bevy.org" @@ -15,28 +15,28 @@ webgpu = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev", optional = true } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev", optional = true } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } # other bytemuck = { version = "1", features = ["derive", "must_cast"] } fixedbitset = "0.5" -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } bitflags = "2.3" radsort = "0.1" nonmax = "0.5" diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 882ec5857c..3e15499dd0 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -16,6 +16,7 @@ mod picking_backend; mod render; mod sprite; mod texture_slice; +mod tilemap_chunk; /// The sprite prelude. /// @@ -40,6 +41,7 @@ pub use picking_backend::*; pub use render::*; pub use sprite::*; pub use texture_slice::*; +pub use tilemap_chunk::*; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, AssetEventSystems, Assets}; @@ -87,7 +89,12 @@ impl Plugin for SpritePlugin { .register_type::() .register_type::() .register_type::() - .add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin)) + .add_plugins(( + Mesh2dRenderPlugin, + ColorMaterialPlugin, + TilemapChunkPlugin, + TilemapChunkMaterialPlugin, + )) .add_systems( PostUpdate, ( diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 3f76b516cd..fa784bd9af 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -967,6 +967,7 @@ impl RenderAsset for PreparedMaterial2d { transparent_draw_functions, material_param, ): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { Ok(prepared) => { diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index a3d9ee3eb2..204dd85e24 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -778,7 +778,7 @@ impl RenderCommand

for SetMesh2dViewBindGroup( _item: &P, - (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _view: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 8ffb12a582..e30c5b1f6c 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -372,7 +372,7 @@ impl ViewNode for Wireframe2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(wireframe_phase) = @@ -473,6 +473,7 @@ impl RenderAsset for RenderWireframeMaterial { source_asset: Self::SourceAsset, _asset_id: AssetId, _param: &mut SystemParamItem, + _previous_asset: Option<&Self>, ) -> Result> { Ok(RenderWireframeMaterial { color: source_asset.color.to_linear().to_f32_array(), diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 57c1acc6bd..bde1a34b63 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -7,7 +7,7 @@ //! //! ## Implementation Notes //! -//! - The `position` reported in `HitData` in in world space, and the `normal` is a normalized +//! - The `position` reported in `HitData` in world space, and the `normal` is a normalized //! vector provided by the target's `GlobalTransform::back()`. use crate::{Anchor, Sprite}; diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 7602addc0b..761e2c628a 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -908,7 +908,7 @@ impl RenderCommand

for SetSpriteViewBindGroup( _item: &P, - (view_uniform, sprite_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, sprite_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -925,7 +925,7 @@ impl RenderCommand

for SetSpriteTextureBindGrou fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (image_bind_groups, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -955,7 +955,7 @@ impl RenderCommand

for DrawSpriteBatch { fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (sprite_meta, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_sprite/src/tilemap_chunk/mod.rs b/crates/bevy_sprite/src/tilemap_chunk/mod.rs new file mode 100644 index 0000000000..f6b35264a4 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap_chunk/mod.rs @@ -0,0 +1,270 @@ +use crate::{AlphaMode2d, Anchor, MeshMaterial2d}; +use bevy_app::{App, Plugin, Update}; +use bevy_asset::{Assets, Handle, RenderAssetUsages}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + lifecycle::HookContext, + query::Changed, + resource::Resource, + system::{Query, ResMut}, + world::DeferredWorld, +}; +use bevy_image::{Image, ImageSampler}; +use bevy_math::{FloatOrd, UVec2, Vec2, Vec3}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + mesh::{Indices, Mesh, Mesh2d, PrimitiveTopology}, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, +}; +use tracing::warn; + +mod tilemap_chunk_material; + +pub use tilemap_chunk_material::*; + +/// Plugin that handles the initialization and updating of tilemap chunks. +/// Adds systems for processing newly added tilemap chunks and updating their indices. +pub struct TilemapChunkPlugin; + +impl Plugin for TilemapChunkPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Update, update_tilemap_chunk_indices); + } +} + +type TilemapChunkMeshCacheKey = (UVec2, FloatOrd, FloatOrd, FloatOrd, FloatOrd); + +/// A resource storing the meshes for each tilemap chunk size. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct TilemapChunkMeshCache(HashMap>); + +/// A component representing a chunk of a tilemap. +/// Each chunk is a rectangular section of tiles that is rendered as a single mesh. +#[derive(Component, Clone, Debug, Default)] +#[require(Anchor)] +#[component(immutable, on_insert = on_insert_tilemap_chunk)] +pub struct TilemapChunk { + /// The size of the chunk in tiles + pub chunk_size: UVec2, + /// The size to use for each tile, not to be confused with the size of a tile in the tileset image. + /// The size of the tile in the tileset image is determined by the tileset image's dimensions. + pub tile_display_size: UVec2, + /// Handle to the tileset image containing all tile textures + pub tileset: Handle, + /// The alpha mode to use for the tilemap chunk + pub alpha_mode: AlphaMode2d, +} + +/// Component storing the indices of tiles within a chunk. +/// Each index corresponds to a specific tile in the tileset. +#[derive(Component, Clone, Debug, Deref, DerefMut)] +pub struct TilemapChunkIndices(pub Vec>); + +fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { + let Some(tilemap_chunk) = world.get::(entity) else { + warn!("TilemapChunk not found for tilemap chunk {}", entity); + return; + }; + + let chunk_size = tilemap_chunk.chunk_size; + let alpha_mode = tilemap_chunk.alpha_mode; + let tileset = tilemap_chunk.tileset.clone(); + + let Some(indices) = world.get::(entity) else { + warn!("TilemapChunkIndices not found for tilemap chunk {}", entity); + return; + }; + + let Some(&anchor) = world.get::(entity) else { + warn!("Anchor not found for tilemap chunk {}", entity); + return; + }; + + let expected_indices_length = chunk_size.element_product() as usize; + if indices.len() != expected_indices_length { + warn!( + "Invalid indices length for tilemap chunk {} of size {}. Expected {}, got {}", + entity, + chunk_size, + indices.len(), + expected_indices_length + ); + return; + } + + let indices_image = make_chunk_image(&chunk_size, &indices.0); + + let display_size = (chunk_size * tilemap_chunk.tile_display_size).as_vec2(); + + let mesh_key: TilemapChunkMeshCacheKey = ( + chunk_size, + FloatOrd(display_size.x), + FloatOrd(display_size.y), + FloatOrd(anchor.as_vec().x), + FloatOrd(anchor.as_vec().y), + ); + + let tilemap_chunk_mesh_cache = world.resource::(); + let mesh = if let Some(mesh) = tilemap_chunk_mesh_cache.get(&mesh_key) { + mesh.clone() + } else { + let mut meshes = world.resource_mut::>(); + meshes.add(make_chunk_mesh(&chunk_size, &display_size, &anchor)) + }; + + let mut images = world.resource_mut::>(); + let indices = images.add(indices_image); + + let mut materials = world.resource_mut::>(); + let material = materials.add(TilemapChunkMaterial { + tileset, + indices, + alpha_mode, + }); + + world + .commands() + .entity(entity) + .insert((Mesh2d(mesh), MeshMaterial2d(material))); +} + +fn update_tilemap_chunk_indices( + query: Query< + ( + Entity, + &TilemapChunk, + &TilemapChunkIndices, + &MeshMaterial2d, + ), + Changed, + >, + mut materials: ResMut>, + mut images: ResMut>, +) { + for (chunk_entity, TilemapChunk { chunk_size, .. }, indices, material) in query { + let expected_indices_length = chunk_size.element_product() as usize; + if indices.len() != expected_indices_length { + warn!( + "Invalid TilemapChunkIndices length for tilemap chunk {} of size {}. Expected {}, got {}", + chunk_entity, + chunk_size, + indices.len(), + expected_indices_length + ); + continue; + } + + // Getting the material mutably to trigger change detection + let Some(material) = materials.get_mut(material.id()) else { + warn!( + "TilemapChunkMaterial not found for tilemap chunk {}", + chunk_entity + ); + continue; + }; + let Some(indices_image) = images.get_mut(&material.indices) else { + warn!( + "TilemapChunkMaterial indices image not found for tilemap chunk {}", + chunk_entity + ); + continue; + }; + let Some(data) = indices_image.data.as_mut() else { + warn!( + "TilemapChunkMaterial indices image data not found for tilemap chunk {}", + chunk_entity + ); + continue; + }; + data.clear(); + data.extend( + indices + .iter() + .copied() + .flat_map(|i| u16::to_ne_bytes(i.unwrap_or(u16::MAX))), + ); + } +} + +fn make_chunk_image(size: &UVec2, indices: &[Option]) -> Image { + Image { + data: Some( + indices + .iter() + .copied() + .flat_map(|i| u16::to_ne_bytes(i.unwrap_or(u16::MAX))) + .collect(), + ), + texture_descriptor: TextureDescriptor { + size: Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + dimension: TextureDimension::D2, + format: TextureFormat::R16Uint, + label: None, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }, + sampler: ImageSampler::nearest(), + texture_view_descriptor: None, + asset_usage: RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD, + copy_on_resize: false, + } +} + +fn make_chunk_mesh(size: &UVec2, display_size: &Vec2, anchor: &Anchor) -> Mesh { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD, + ); + + let offset = display_size * (Vec2::splat(-0.5) - anchor.as_vec()); + + let num_quads = size.element_product() as usize; + let quad_size = display_size / size.as_vec2(); + + let mut positions = Vec::with_capacity(4 * num_quads); + let mut uvs = Vec::with_capacity(4 * num_quads); + let mut indices = Vec::with_capacity(6 * num_quads); + + for y in 0..size.y { + for x in 0..size.x { + let i = positions.len() as u32; + + let p0 = offset + quad_size * UVec2::new(x, y).as_vec2(); + let p1 = p0 + quad_size; + + positions.extend([ + Vec3::new(p0.x, p0.y, 0.0), + Vec3::new(p1.x, p0.y, 0.0), + Vec3::new(p0.x, p1.y, 0.0), + Vec3::new(p1.x, p1.y, 0.0), + ]); + + uvs.extend([ + Vec2::new(0.0, 1.0), + Vec2::new(1.0, 1.0), + Vec2::new(0.0, 0.0), + Vec2::new(1.0, 0.0), + ]); + + indices.extend([i, i + 2, i + 1]); + indices.extend([i + 3, i + 1, i + 2]); + } + } + + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); + mesh.insert_indices(Indices::U32(indices)); + + mesh +} diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs new file mode 100644 index 0000000000..71af0244c8 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs @@ -0,0 +1,69 @@ +use crate::{AlphaMode2d, Material2d, Material2dKey, Material2dPlugin}; +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle}; +use bevy_image::Image; +use bevy_reflect::prelude::*; +use bevy_render::{ + mesh::{Mesh, MeshVertexBufferLayoutRef}, + render_resource::*, +}; + +/// Plugin that adds support for tilemap chunk materials. +pub struct TilemapChunkMaterialPlugin; + +impl Plugin for TilemapChunkMaterialPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "tilemap_chunk_material.wgsl"); + + app.add_plugins(Material2dPlugin::::default()); + } +} + +/// Material used for rendering tilemap chunks. +/// +/// This material is used internally by the tilemap system to render chunks of tiles +/// efficiently using a single draw call per chunk. +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +pub struct TilemapChunkMaterial { + pub alpha_mode: AlphaMode2d, + + #[texture(0, dimension = "2d_array")] + #[sampler(1)] + pub tileset: Handle, + + #[texture(2, sample_type = "u_int")] + pub indices: Handle, +} + +impl Material2d for TilemapChunkMaterial { + fn fragment_shader() -> ShaderRef { + ShaderRef::Path( + AssetPath::from_path_buf(embedded_path!("tilemap_chunk_material.wgsl")) + .with_source("embedded"), + ) + } + + fn vertex_shader() -> ShaderRef { + ShaderRef::Path( + AssetPath::from_path_buf(embedded_path!("tilemap_chunk_material.wgsl")) + .with_source("embedded"), + ) + } + + fn alpha_mode(&self) -> AlphaMode2d { + self.alpha_mode + } + + fn specialize( + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + _key: Material2dKey, + ) -> Result<(), SpecializedMeshPipelineError> { + let vertex_layout = layout.0.get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_UV_0.at_shader_location(1), + ])?; + descriptor.vertex.buffers = vec![vertex_layout]; + Ok(()) + } +} diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl new file mode 100644 index 0000000000..7424995e22 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl @@ -0,0 +1,58 @@ +#import bevy_sprite::{ + mesh2d_functions as mesh_functions, + mesh2d_view_bindings::view, +} + +struct Vertex { + @builtin(instance_index) instance_index: u32, + @builtin(vertex_index) vertex_index: u32, + @location(0) position: vec3, + @location(1) uv: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, + @location(1) tile_index: u32, +} + +@group(2) @binding(0) var tileset: texture_2d_array; +@group(2) @binding(1) var tileset_sampler: sampler; +@group(2) @binding(2) var tile_indices: texture_2d; + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + + let world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); + let world_position = mesh_functions::mesh2d_position_local_to_world( + world_from_local, + vec4(vertex.position, 1.0) + ); + + out.position = mesh_functions::mesh2d_position_world_to_clip(world_position); + out.uv = vertex.uv; + out.tile_index = vertex.vertex_index / 4u; + + return out; +} + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + let chunk_size = textureDimensions(tile_indices, 0); + let tile_xy = vec2( + in.tile_index % chunk_size.x, + in.tile_index / chunk_size.x + ); + let tile_id = textureLoad(tile_indices, tile_xy, 0).r; + + if tile_id == 0xffffu { + discard; + } + + let color = textureSample(tileset, tileset_sampler, in.uv, tile_id); + if color.a < 0.001 { + discard; + } + return color; +} \ No newline at end of file diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml index fd780fc486..17f9d6c787 100644 --- a/crates/bevy_state/Cargo.toml +++ b/crates/bevy_state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_state" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Finite state machines for Bevy" homepage = "https://bevy.org" @@ -46,12 +46,12 @@ critical-section = [ [dependencies] # bevy -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_state_macros = { path = "macros", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_state_macros = { path = "macros", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } variadics_please = "1.1" # other diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml index 2f569f395e..2ab531a9b2 100644 --- a/crates/bevy_state/macros/Cargo.toml +++ b/crates/bevy_state/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_state_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" description = "Macros for bevy_state" edition = "2024" license = "MIT OR Apache-2.0" @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = { version = "2.0", features = ["full"] } quote = "1.0" diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 05116cbcc5..c13abceeb9 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -116,7 +116,7 @@ impl AppExtStates for SubApp { } } else { let name = core::any::type_name::(); - warn!("State {} is already initialized.", name); + warn!("State {name} is already initialized."); } self @@ -178,7 +178,7 @@ impl AppExtStates for SubApp { } } else { let name = core::any::type_name::(); - warn!("Computed state {} is already initialized.", name); + warn!("Computed state {name} is already initialized."); } self @@ -209,7 +209,7 @@ impl AppExtStates for SubApp { } } else { let name = core::any::type_name::(); - warn!("Sub state {} is already initialized.", name); + warn!("Sub state {name} is already initialized."); } self @@ -222,7 +222,7 @@ impl AppExtStates for SubApp { .contains_resource::>>() { let name = core::any::type_name::(); - warn!("State scoped entities are enabled for state `{}`, but the state isn't installed in the app!", name); + warn!("State scoped entities are enabled for state `{name}`, but the state isn't installed in the app!"); } // Note: We work with `StateTransition` in set diff --git a/crates/bevy_state/src/commands.rs b/crates/bevy_state/src/commands.rs index d9da362b62..4617011726 100644 --- a/crates/bevy_state/src/commands.rs +++ b/crates/bevy_state/src/commands.rs @@ -21,7 +21,7 @@ impl CommandsStatesExt for Commands<'_, '_> { let mut next = w.resource_mut::>(); if let NextState::Pending(prev) = &*next { if *prev != state { - debug!("overwriting next state {:?} with {:?}", prev, state); + debug!("overwriting next state {prev:?} with {state:?}"); } } next.set(state); diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs index 9267478281..61ee0627a3 100644 --- a/crates/bevy_state/src/state/mod.rs +++ b/crates/bevy_state/src/state/mod.rs @@ -642,6 +642,203 @@ mod tests { } } + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum MultiSourceComputedState { + FromSimpleBTrue, + FromSimple2B2, + FromBoth, + } + + impl ComputedStates for MultiSourceComputedState { + type SourceStates = (SimpleState, SimpleState2); + + fn compute((simple_state, simple_state2): (SimpleState, SimpleState2)) -> Option { + match (simple_state, simple_state2) { + // If both are in their special states, prioritize the "both" variant. + (SimpleState::B(true), SimpleState2::B2) => Some(Self::FromBoth), + // If only SimpleState is B(true). + (SimpleState::B(true), _) => Some(Self::FromSimpleBTrue), + // If only SimpleState2 is B2. + (_, SimpleState2::B2) => Some(Self::FromSimple2B2), + // Otherwise, no computed state. + _ => None, + } + } + } + + /// This test ensures that [`ComputedStates`] with multiple source states + /// react when any source changes. + #[test] + fn computed_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceComputedState::register_computed_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceComputedState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The computed state should exist because SimpleState changed to + // B(true). + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + + // Reset SimpleState to A - computed state should be removed. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The computed state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimple2B2 + ); + + // Test that changes to both states work. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + } + + // Test SubState that depends on multiple source states. + #[derive(PartialEq, Eq, Debug, Default, Hash, Clone)] + enum MultiSourceSubState { + #[default] + Active, + } + + impl SubStates for MultiSourceSubState { + type SourceStates = (SimpleState, SimpleState2); + + fn should_exist( + (simple_state, simple_state2): (SimpleState, SimpleState2), + ) -> Option { + // SubState should exist when: + // - SimpleState is B(true), OR + // - SimpleState2 is B2 + match (simple_state, simple_state2) { + (SimpleState::B(true), _) | (_, SimpleState2::B2) => Some(Self::Active), + _ => None, + } + } + } + + impl States for MultiSourceSubState { + const DEPENDENCY_DEPTH: usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + } + + impl FreelyMutableState for MultiSourceSubState {} + + /// This test ensures that [`SubStates`] with multiple source states react + /// when any source changes. + #[test] + fn sub_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceSubState::register_sub_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceSubState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceSubState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The sub state should exist because SimpleState changed to B(true). + assert!(world.contains_resource::>()); + + // Reset to initial state. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceSubState creation. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The sub state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + + // Finally, test that it works when both change simultaneously. + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + // After this transition, the state should not exist since SimpleState + // is B(false). + assert!(!world.contains_resource::>()); + + // Change both at the same time. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert!(world.contains_resource::>()); + } + #[test] fn check_transition_orders() { let mut world = World::new(); diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs index 69a6c41b3d..3cf1e1d260 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -293,7 +293,7 @@ macro_rules! impl_state_set_sealed_tuples { current_state_res: Option>>, next_state_res: Option>>, ($($val),*,): ($(Option>>),*,)| { - let parent_changed = ($($evt.read().last().is_some())&&*); + let parent_changed = ($($evt.read().last().is_some())||*); let next_state = take_next_state(next_state_res); if !parent_changed && next_state.is_none() { diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index dfe711f245..1ee21826c3 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -1,7 +1,7 @@ use core::{marker::PhantomData, mem}; use bevy_ecs::{ - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, @@ -55,11 +55,11 @@ pub struct OnTransition { #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] pub struct StateTransition; -/// Event sent when any state transition of `S` happens. +/// A [`BufferedEvent`] sent when any state transition of `S` happens. /// This includes identity transitions, where `exited` and `entered` have the same value. /// /// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Event, BufferedEvent)] pub struct StateTransitionEvent { /// The state being exited. pub exited: Option, diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index b11d8e79df..adba1ca6b6 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; use bevy_app::{App, SubApp}; use bevy_ecs::{ - event::{Event, EventReader, Events}, + event::{BufferedEvent, EventReader, Events}, resource::Resource, system::Commands, world::World, @@ -12,7 +12,7 @@ use bevy_platform::collections::HashMap; use crate::state::{OnEnter, OnExit, StateTransitionEvent, States}; -fn clear_event_queue(w: &mut World) { +fn clear_event_queue(w: &mut World) { if let Some(mut queue) = w.get_resource_mut::>() { queue.clear(); } @@ -33,7 +33,7 @@ struct StateScopedEvents { } impl StateScopedEvents { - fn add_event(&mut self, state: S, transition_type: TransitionType) { + fn add_event(&mut self, state: S, transition_type: TransitionType) { let map = match transition_type { TransitionType::OnExit => &mut self.on_exit, TransitionType::OnEnter => &mut self.on_enter, @@ -106,7 +106,7 @@ fn clear_events_on_enter_state( }); } -fn clear_events_on_state_transition( +fn clear_events_on_state_transition( app: &mut SubApp, _p: PhantomData, state: S, @@ -128,7 +128,7 @@ fn clear_events_on_state_transition( /// Extension trait for [`App`] adding methods for registering state scoped events. pub trait StateScopedEventsAppExt { - /// Clears an [`Event`] when exiting the specified `state`. + /// Clears an [`BufferedEvent`] when exiting the specified `state`. /// /// Note that event cleanup is ambiguously ordered relative to /// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup, @@ -136,9 +136,9 @@ pub trait StateScopedEventsAppExt { /// All of these (state scoped entities and events cleanup, and `OnExit`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::ExitSchedules`. - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; - /// Clears an [`Event`] when entering the specified `state`. + /// Clears an [`BufferedEvent`] when entering the specified `state`. /// /// Note that event cleanup is ambiguously ordered relative to /// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup, @@ -146,11 +146,11 @@ pub trait StateScopedEventsAppExt { /// All of these (state scoped entities and events cleanup, and `OnEnter`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::EnterSchedules`. - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; } impl StateScopedEventsAppExt for App { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -160,7 +160,7 @@ impl StateScopedEventsAppExt for App { self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -172,12 +172,12 @@ impl StateScopedEventsAppExt for App { } impl StateScopedEventsAppExt for SubApp { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnExit); self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnEnter); self } @@ -187,6 +187,7 @@ impl StateScopedEventsAppExt for SubApp { mod tests { use super::*; use crate::app::StatesPlugin; + use bevy_ecs::event::{BufferedEvent, Event}; use bevy_state::prelude::*; #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)] @@ -196,10 +197,10 @@ mod tests { B, } - #[derive(Event, Debug)] + #[derive(Event, BufferedEvent, Debug)] struct StandardEvent; - #[derive(Event, Debug)] + #[derive(Event, BufferedEvent, Debug)] struct StateScopedEvent; #[test] diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index ad162a7ef7..e28d7fc88d 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_tasks" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A task executor for Bevy Engine" homepage = "https://bevy.org" @@ -47,7 +47,7 @@ web = [ ] [dependencies] -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } @@ -55,7 +55,7 @@ futures-lite = { version = "2.0.1", default-features = false, features = [ "alloc", ] } async-task = { version = "4.4.0", default-features = false } -derive_more = { version = "1", default-features = false, features = [ +derive_more = { version = "2", default-features = false, features = [ "deref", "deref_mut", ] } diff --git a/crates/bevy_tasks/src/executor.rs b/crates/bevy_tasks/src/executor.rs index 9a9f4f9dfa..01bbe4a669 100644 --- a/crates/bevy_tasks/src/executor.rs +++ b/crates/bevy_tasks/src/executor.rs @@ -65,9 +65,11 @@ impl LocalExecutor<'_> { } impl UnwindSafe for Executor<'_> {} + impl RefUnwindSafe for Executor<'_> {} impl UnwindSafe for LocalExecutor<'_> {} + impl RefUnwindSafe for LocalExecutor<'_> {} impl fmt::Debug for Executor<'_> { diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index ce9aa78883..66899ef36f 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -32,6 +32,7 @@ pub use conditional_send::*; /// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. Wasm), /// futures aren't Send. pub trait ConditionalSendFuture: Future + ConditionalSend {} + impl ConditionalSendFuture for T {} use alloc::boxed::Box; diff --git a/crates/bevy_tasks/src/thread_executor.rs b/crates/bevy_tasks/src/thread_executor.rs index 48fb3e2861..86d2ab280d 100644 --- a/crates/bevy_tasks/src/thread_executor.rs +++ b/crates/bevy_tasks/src/thread_executor.rs @@ -24,7 +24,7 @@ use futures_lite::Future; /// // we cannot get the ticker from another thread /// let not_thread_ticker = thread_executor.ticker(); /// assert!(not_thread_ticker.is_none()); -/// +/// /// // but we can spawn tasks from another thread /// thread_executor.spawn(async move { /// count_clone.fetch_add(1, Ordering::Relaxed); @@ -98,6 +98,7 @@ pub struct ThreadExecutorTicker<'task, 'ticker> { // make type not send or sync _marker: PhantomData<*const ()>, } + impl<'task, 'ticker> ThreadExecutorTicker<'task, 'ticker> { /// Tick the thread executor. pub async fn tick(&self) { diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 9a8c8e8eee..c71fb5ce0c 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_text" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides text functionality for Bevy Engine" homepage = "https://bevy.org" @@ -13,21 +13,21 @@ default_font = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 494725b08c..7c8e840ace 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_time" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides time functionality for Bevy Engine" homepage = "https://bevy.org" @@ -48,10 +48,10 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other crossbeam-channel = { version = "0.5.0", default-features = false, features = [ diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index 6e7b3b3991..c4118b876c 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -185,7 +185,10 @@ mod tests { use crate::{Fixed, Time, TimePlugin, TimeUpdateStrategy, Virtual}; use bevy_app::{App, FixedUpdate, Startup, Update}; use bevy_ecs::{ - event::{Event, EventReader, EventRegistry, EventWriter, Events, ShouldUpdateEvents}, + event::{ + BufferedEvent, Event, EventReader, EventRegistry, EventWriter, Events, + ShouldUpdateEvents, + }, resource::Resource, system::{Local, Res, ResMut}, }; @@ -193,7 +196,7 @@ mod tests { use core::time::Duration; use std::println; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent { sender: std::sync::mpsc::Sender, } @@ -206,7 +209,7 @@ mod tests { } } - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct DummyEvent; #[derive(Resource, Default)] diff --git a/crates/bevy_time/src/time.rs b/crates/bevy_time/src/time.rs index d0845e22d3..8c4456f0e3 100644 --- a/crates/bevy_time/src/time.rs +++ b/crates/bevy_time/src/time.rs @@ -119,7 +119,7 @@ use { /// # use bevy_ecs::prelude::*; /// # use bevy_time::prelude::*; /// # -/// #[derive(Event)] +/// #[derive(Event, BufferedEvent)] /// struct PauseEvent(bool); /// /// fn pause_system(mut time: ResMut>, mut events: EventReader) { diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 6801f5734b..bec0f4fb50 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_transform" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides transform functionality for Bevy Engine" homepage = "https://bevy.org" @@ -10,22 +10,22 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, optional = true } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false, optional = true } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev", default-features = false, optional = true } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, optional = true } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false, optional = true } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev", default-features = false, optional = true } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false, optional = true } serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } [dev-dependencies] -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "approx", ] } approx = "0.5.1" diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index b10d5a9d1a..cd7db6ef71 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -115,7 +115,7 @@ impl GlobalTransform { /// Returns the 3d affine transformation matrix as a [`Mat4`]. #[inline] - pub fn compute_matrix(&self) -> Mat4 { + pub fn to_matrix(&self) -> Mat4 { Mat4::from(self.0) } @@ -139,8 +139,9 @@ impl GlobalTransform { } } - /// Returns the isometric part of the transformation as an [isometry]. Any scaling done by the - /// transformation will be ignored. + /// Computes a Scale-Rotation-Translation decomposition of the transformation and returns + /// the isometric part as an [isometry]. Any scaling done by the transformation will be ignored. + /// Note: this is a somewhat costly and lossy conversion. /// /// The transform is expected to be non-degenerate and without shearing, or the output /// will be invalid. diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 7873ae743c..bc161e9e8c 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -256,10 +256,10 @@ impl Transform { self } - /// Returns the 3d affine transformation matrix from this transforms translation, + /// Computes the 3d affine transformation matrix from this transform's translation, /// rotation, and scale. #[inline] - pub fn compute_matrix(&self) -> Mat4 { + pub fn to_matrix(&self) -> Mat4 { Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) } diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 5224d90296..3ffe730fbd 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ui" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A custom ECS-driven UI framework built specifically for Bevy Engine" homepage = "https://bevy.org" @@ -10,25 +10,25 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -38,7 +38,7 @@ serde = { version = "1", features = ["derive"], optional = true } uuid = { version = "1.1", features = ["v4"], optional = true } bytemuck = { version = "1.5", features = ["derive"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } nonmax = "0.5" smallvec = "1.11" accesskit = "0.19" @@ -51,6 +51,7 @@ serialize = [ "smallvec/serde", "bevy_math/serialize", "bevy_platform/serialize", + "bevy_render/serialize", ] bevy_ui_picking_backend = ["bevy_picking", "dep:uuid"] bevy_ui_debug = [] diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index 969e062cd7..eb1d255cc7 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -3,6 +3,7 @@ use bevy_color::{Color, Srgba}; use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::prelude::*; +use bevy_utils::default; use core::{f32, f32::consts::TAU}; /// A color stop for a gradient @@ -205,7 +206,7 @@ impl Default for AngularColorStop { /// A linear gradient /// /// -#[derive(Clone, PartialEq, Debug, Reflect)] +#[derive(Default, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", @@ -213,6 +214,8 @@ impl Default for AngularColorStop { reflect(Serialize, Deserialize) )] pub struct LinearGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The direction of the gradient in radians. /// An angle of `0.` points upward, with the value increasing in the clockwise direction. pub angle: f32, @@ -240,7 +243,11 @@ impl LinearGradient { /// Create a new linear gradient pub fn new(angle: f32, stops: Vec) -> Self { - Self { angle, stops } + Self { + angle, + stops, + color_space: InterpolationColorSpace::default(), + } } /// A linear gradient transitioning from bottom to top @@ -248,6 +255,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP, stops, + color_space: InterpolationColorSpace::default(), } } @@ -256,6 +264,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -264,6 +273,7 @@ impl LinearGradient { Self { angle: Self::TO_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -272,6 +282,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -280,6 +291,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM, stops, + color_space: InterpolationColorSpace::default(), } } @@ -288,6 +300,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -296,6 +309,7 @@ impl LinearGradient { Self { angle: Self::TO_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -304,6 +318,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -312,8 +327,14 @@ impl LinearGradient { Self { angle: degrees.to_radians(), stops, + color_space: InterpolationColorSpace::default(), } } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } /// A radial gradient @@ -327,6 +348,8 @@ impl LinearGradient { reflect(Serialize, Deserialize) )] pub struct RadialGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The center of the radial gradient pub position: UiPosition, /// Defines the end shape of the radial gradient @@ -339,11 +362,17 @@ impl RadialGradient { /// Create a new radial gradient pub fn new(position: UiPosition, shape: RadialGradientShape, stops: Vec) -> Self { Self { + color_space: default(), position, shape, stops, } } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } impl Default for RadialGradient { @@ -352,6 +381,7 @@ impl Default for RadialGradient { position: UiPosition::CENTER, shape: RadialGradientShape::ClosestCorner, stops: Vec::new(), + color_space: default(), } } } @@ -359,7 +389,7 @@ impl Default for RadialGradient { /// A conic gradient /// /// -#[derive(Clone, PartialEq, Debug, Reflect)] +#[derive(Default, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", @@ -367,6 +397,8 @@ impl Default for RadialGradient { reflect(Serialize, Deserialize) )] pub struct ConicGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The starting angle of the gradient in radians pub start: f32, /// The center of the conic gradient @@ -379,6 +411,7 @@ impl ConicGradient { /// Create a new conic gradient pub fn new(position: UiPosition, stops: Vec) -> Self { Self { + color_space: default(), start: 0., position, stops, @@ -396,6 +429,11 @@ impl ConicGradient { self.position = position; self } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } #[derive(Clone, PartialEq, Debug, Reflect)] @@ -573,3 +611,79 @@ impl RadialGradientShape { } } } + +/// The color space used for interpolation. +#[derive(Default, Copy, Clone, Hash, Debug, PartialEq, Eq, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum InterpolationColorSpace { + /// Interpolates in `OKLab` space. + #[default] + OkLab, + /// Interpolates in OKLCH space, taking the shortest hue path. + OkLch, + /// Interpolates in OKLCH space, taking the longest hue path. + OkLchLong, + /// Interpolates in sRGB space. + Srgb, + /// Interpolates in linear sRGB space. + LinearRgb, +} + +/// Set the color space used for interpolation. +pub trait InColorSpace: Sized { + /// Interpolate in the given `color_space`. + fn in_color_space(self, color_space: InterpolationColorSpace) -> Self; + + /// Interpolate in `OKLab` space. + fn in_oklab(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLab) + } + + /// Interpolate in OKLCH space (short hue path). + fn in_oklch(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLch) + } + + /// Interpolate in OKLCH space (long hue path). + fn in_oklch_long(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLchLong) + } + + /// Interpolate in sRGB space. + fn in_srgb(self) -> Self { + self.in_color_space(InterpolationColorSpace::Srgb) + } + + /// Interpolate in linear sRGB space. + fn in_linear_rgb(self) -> Self { + self.in_color_space(InterpolationColorSpace::LinearRgb) + } +} + +impl InColorSpace for LinearGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} + +impl InColorSpace for RadialGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} + +impl InColorSpace for ConicGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs new file mode 100644 index 0000000000..b50f4cc245 --- /dev/null +++ b/crates/bevy_ui/src/interaction_states.rs @@ -0,0 +1,82 @@ +/// This module contains components that are used to track the interaction state of UI widgets. +use bevy_a11y::AccessibilityNode; +use bevy_ecs::{ + component::Component, + lifecycle::{Add, Remove}, + observer::On, + world::DeferredWorld, +}; + +/// A component indicating that a widget is disabled and should be "grayed out". +/// This is used to prevent user interaction with the widget. It should not, however, prevent +/// the widget from being updated or rendered, or from acquiring keyboard focus. +/// +/// For apps which support a11y: if a widget (such as a slider) contains multiple entities, +/// the `InteractionDisabled` component should be added to the root entity of the widget - the +/// same entity that contains the `AccessibilityNode` component. This will ensure that +/// the a11y tree is updated correctly. +#[derive(Component, Debug, Clone, Copy, Default)] +pub struct InteractionDisabled; + +pub(crate) fn on_add_disabled(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_disabled(); + } +} + +pub(crate) fn on_remove_disabled( + trigger: On, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_disabled(); + } +} + +/// Component that indicates whether a button or widget is currently in a pressed or "held down" +/// state. +#[derive(Component, Default, Debug)] +pub struct Pressed; + +/// Component that indicates that a widget can be checked. +#[derive(Component, Default, Debug)] +pub struct Checkable; + +/// Component that indicates whether a checkbox or radio button is in a checked state. +#[derive(Component, Default, Debug)] +pub struct Checked; + +pub(crate) fn on_add_checkable(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let checked = entity.get::().is_some(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(match checked { + true => accesskit::Toggled::True, + false => accesskit::Toggled::False, + }); + } +} + +pub(crate) fn on_remove_checkable(trigger: On, mut world: DeferredWorld) { + // Remove the 'toggled' attribute entirely. + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_toggled(); + } +} + +pub(crate) fn on_add_checked(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::True); + } +} + +pub(crate) fn on_remove_checked(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::False); + } +} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index be0649c038..47d396b201 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -10,6 +10,7 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`], [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) +pub mod interaction_states; pub mod measurement; pub mod ui_material; pub mod update; @@ -38,6 +39,7 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; +pub use interaction_states::{Checkable, Checked, InteractionDisabled, Pressed}; pub use layout::*; pub use measurement::*; pub use render::*; @@ -319,6 +321,13 @@ fn build_text_interop(app: &mut App) { app.add_plugins(accessibility::AccessibilityPlugin); + app.add_observer(interaction_states::on_add_disabled) + .add_observer(interaction_states::on_remove_disabled) + .add_observer(interaction_states::on_add_checkable) + .add_observer(interaction_states::on_remove_checkable) + .add_observer(interaction_states::on_add_checked) + .add_observer(interaction_states::on_remove_checked); + app.configure_sets( PostUpdate, AmbiguousWithText.ambiguous_with(widget::text_system), diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index bd818c7d5b..e1c845d481 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -140,6 +140,7 @@ pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 { #[derive(Clone, Copy, Hash, PartialEq, Eq)] pub struct UiGradientPipelineKey { anti_alias: bool, + color_space: InterpolationColorSpace, pub hdr: bool, } @@ -180,10 +181,18 @@ impl SpecializedRenderPipeline for GradientPipeline { VertexFormat::Float32, ], ); + let color_space = match key.color_space { + InterpolationColorSpace::OkLab => "IN_OKLAB", + InterpolationColorSpace::OkLch => "IN_OKLCH", + InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG", + InterpolationColorSpace::Srgb => "IN_SRGB", + InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB", + }; + let shader_defs = if key.anti_alias { - vec!["ANTI_ALIAS".into()] + vec![color_space.into(), "ANTI_ALIAS".into()] } else { - Vec::new() + vec![color_space.into()] }; RenderPipelineDescriptor { @@ -254,6 +263,7 @@ pub struct ExtractedGradient { /// Ordering: left, top, right, bottom. pub border: BorderRect, pub resolved_gradient: ResolvedGradient, + pub color_space: InterpolationColorSpace, } #[derive(Resource, Default)] @@ -422,7 +432,11 @@ pub fn extract_gradients( continue; } match gradient { - Gradient::Linear(LinearGradient { angle, stops }) => { + Gradient::Linear(LinearGradient { + color_space, + angle, + stops, + }) => { let length = compute_gradient_line_length(*angle, uinode.size); let range_start = extracted_color_stops.0.len(); @@ -452,9 +466,11 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, resolved_gradient: ResolvedGradient::Linear { angle: *angle }, + color_space: *color_space, }); } Gradient::Radial(RadialGradient { + color_space, position: center, shape, stops, @@ -500,9 +516,11 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, resolved_gradient: ResolvedGradient::Radial { center: c, size }, + color_space: *color_space, }); } Gradient::Conic(ConicGradient { + color_space, start, position: center, stops, @@ -557,6 +575,7 @@ pub fn extract_gradients( start: *start, center: g_start, }, + color_space: *color_space, }); } } @@ -601,6 +620,7 @@ pub fn queue_gradient( &gradients_pipeline, UiGradientPipelineKey { anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), + color_space: gradient.color_space, hdr: view.hdr, }, ); diff --git a/crates/bevy_ui/src/render/gradient.wgsl b/crates/bevy_ui/src/render/gradient.wgsl index 0223836f2d..074cf35a35 100644 --- a/crates/bevy_ui/src/render/gradient.wgsl +++ b/crates/bevy_ui/src/render/gradient.wgsl @@ -122,6 +122,89 @@ fn mix_linear_rgb_in_srgb_space(a: vec4, b: vec4, t: f32) -> vec4 return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); } +fn linear_rgb_to_oklab(c: vec4) -> vec4 { + let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.); + let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.); + let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.); + return vec4( + 0.21045426 * l + 0.7936178 * m - 0.004072047 * s, + 1.9779985 * l - 2.4285922 * m + 0.4505937 * s, + 0.025904037 * l + 0.78277177 * m - 0.80867577 * s, + c.w + ); +} + +fn oklab_to_linear_rgba(c: vec4) -> vec4 { + let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z; + let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z; + let s_ = c.x - 0.08948418 * c.y - 1.2914855 * c.z; + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + return vec4( + 4.0767417 * l - 3.3077116 * m + 0.23096994 * s, + -1.268438 * l + 2.6097574 * m - 0.34131938 * s, + -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s, + c.w + ); +} + +fn mix_linear_rgb_in_oklab_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklab_to_linear_rgba(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t)); +} + +/// hue is left in radians and not converted to degrees +fn linear_rgb_to_oklch(c: vec4) -> vec4 { + let o = linear_rgb_to_oklab(c); + let chroma = sqrt(o.y * o.y + o.z * o.z); + let hue = atan2(o.z, o.y); + return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.w); +} + +fn oklch_to_linear_rgb(c: vec4) -> vec4 { + let a = c.y * cos(c.z); + let b = c.y * sin(c.z); + return oklab_to_linear_rgba(vec4(c.x, a, b, c.w)); +} + +fn rem_euclid(a: f32, b: f32) -> f32 { + return ((a % b) + b) % b; +} + +fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + diff * t, TAU); +} + +fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU); +} + +fn mix_oklch(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_oklch_long(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue_long(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_linear_rgb_in_oklch_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + +fn mix_linear_rgb_in_oklch_space_long(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + // These functions are used to calculate the distance in gradient space from the start of the gradient to the point. // The distance in gradient space is then used to interpolate between the start and end colors. @@ -192,7 +275,16 @@ fn interpolate_gradient( } else { t = 0.5 * (1 + (t - hint) / (1.0 - hint)); } - - // Only color interpolation in SRGB space is supported atm. + +#ifdef IN_SRGB return mix_linear_rgb_in_srgb_space(start_color, end_color, t); +#else ifdef IN_OKLAB + return mix_linear_rgb_in_oklab_space(start_color, end_color, t); +#else ifdef IN_OKLCH + return mix_linear_rgb_in_oklch_space(start_color, end_color, t); +#else ifdef IN_OKLCH_LONG + return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t); +#else + return mix(start_color, end_color, t); +#endif } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index ebdeacccf9..5d2201e609 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -276,7 +276,7 @@ impl RenderCommand

fn render<'w>( _item: &P, _view: (), - material_handle: Option>, + material_handle: Option>, materials: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -581,6 +581,7 @@ impl RenderAsset for PreparedUiMaterial { material: Self::SourceAsset, _: AssetId, (render_device, pipeline, material_param): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) { Ok(prepared) => Ok(PreparedUiMaterial { diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 2ae820a1a1..6418f69ff8 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1043,9 +1043,11 @@ pub enum BoxSizing { /// Length styles like width and height refer to the "content box" size (size excluding padding and border) ContentBox, } + impl BoxSizing { pub const DEFAULT: Self = Self::BorderBox; } + impl Default for BoxSizing { fn default() -> Self { Self::DEFAULT @@ -2130,6 +2132,16 @@ impl BorderColor { } } + /// Helper to set all border colors to a given color. + pub fn set_all(&mut self, color: impl Into) -> &mut Self { + let color: Color = color.into(); + self.top = color; + self.bottom = color; + self.left = color; + self.right = color; + self + } + /// Check if all contained border colors are transparent pub fn is_fully_transparent(&self) -> bool { self.top.is_fully_transparent() @@ -2865,6 +2877,8 @@ impl ComputedNodeTarget { } /// Adds a shadow behind text +/// +/// Not supported by `Text2d` #[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] #[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct TextShadow { diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 53eda0d358..4e74e6ea94 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_utils" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A collection of utils for Bevy Engine" homepage = "https://bevy.org" @@ -16,9 +16,14 @@ wgpu_wrapper = ["dep:send_wrapper"] # Provides access to the `Parallel` type. parallel = ["bevy_platform/std", "dep:thread_local"] -[dependencies] -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +std = ["disqualified/alloc"] +debug = [] + +[dependencies] +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } + +disqualified = { version = "1.0", default-features = false } thread_local = { version = "1.0", optional = true } [target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] diff --git a/crates/bevy_utils/src/debug_info.rs b/crates/bevy_utils/src/debug_info.rs new file mode 100644 index 0000000000..c79c5ebe60 --- /dev/null +++ b/crates/bevy_utils/src/debug_info.rs @@ -0,0 +1,102 @@ +use alloc::{borrow::Cow, fmt, string::String}; +#[cfg(feature = "debug")] +use core::any::type_name; +use disqualified::ShortName; + +#[cfg(not(feature = "debug"))] +const FEATURE_DISABLED: &'static str = "Enable the debug feature to see the name"; + +/// Wrapper to help debugging ECS issues. This is used to display the names of systems, components, ... +/// +/// * If the `debug` feature is enabled, the actual name will be used +/// * If it is disabled, a string mentioning the disabled feature will be used +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DebugName { + #[cfg(feature = "debug")] + name: Cow<'static, str>, +} + +impl fmt::Display for DebugName { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "debug")] + f.write_str(self.name.as_ref())?; + #[cfg(not(feature = "debug"))] + f.write_str(FEATURE_DISABLED)?; + + Ok(()) + } +} + +impl DebugName { + /// Create a new `DebugName` from a `&str` + /// + /// The value will be ignored if the `debug` feature is not enabled + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + pub fn borrowed(value: &'static str) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Borrowed(value), + } + } + + /// Create a new `DebugName` from a `String` + /// + /// The value will be ignored if the `debug` feature is not enabled + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + pub fn owned(value: String) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Owned(value), + } + } + + /// Create a new `DebugName` from a type by using its [`core::any::type_name`] + /// + /// The value will be ignored if the `debug` feature is not enabled + pub fn type_name() -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Borrowed(type_name::()), + } + } + + /// Get the [`ShortName`] corresping to this debug name + /// + /// The value will be a static string if the `debug` feature is not enabled + pub fn shortname(&self) -> ShortName { + #[cfg(feature = "debug")] + return ShortName(self.name.as_ref()); + #[cfg(not(feature = "debug"))] + return ShortName(FEATURE_DISABLED); + } + + /// Return the string hold by this `DebugName` + /// + /// This is intended for debugging purpose, and only available if the `debug` feature is enabled + #[cfg(feature = "debug")] + pub fn as_string(&self) -> String { + self.name.clone().into_owned() + } +} + +impl From> for DebugName { + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + fn from(value: Cow<'static, str>) -> Self { + Self { + #[cfg(feature = "debug")] + name: value, + } + } +} + +impl From for DebugName { + fn from(value: String) -> Self { + Self::owned(value) + } +} + +impl From<&'static str> for DebugName { + fn from(value: &'static str) -> Self { + Self::borrowed(value) + } +} diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index e3bb07a512..58979139bb 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -43,12 +43,14 @@ cfg::parallel! { /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { + pub use crate::debug_info::DebugName; pub use crate::default; } #[cfg(feature = "wgpu_wrapper")] mod wgpu_wrapper; +mod debug_info; mod default; mod once; diff --git a/crates/bevy_utils/src/map.rs b/crates/bevy_utils/src/map.rs index ca74e34dbb..3b54a357aa 100644 --- a/crates/bevy_utils/src/map.rs +++ b/crates/bevy_utils/src/map.rs @@ -1,7 +1,7 @@ use core::{any::TypeId, hash::Hash}; use bevy_platform::{ - collections::HashMap, + collections::{hash_map::Entry, HashMap}, hash::{Hashed, NoOpHash, PassHash}, }; @@ -38,6 +38,78 @@ impl PreHashMapExt for PreHashMap = HashMap; +/// Extension trait to make use of [`TypeIdMap`] more ergonomic. +/// +/// Each function on this trait is a trivial wrapper for a function +/// on [`HashMap`], replacing a `TypeId` key with a +/// generic parameter `T`. +/// +/// # Examples +/// +/// ```rust +/// # use std::any::TypeId; +/// # use bevy_utils::TypeIdMap; +/// use bevy_utils::TypeIdMapExt; +/// +/// struct MyType; +/// +/// // Using the built-in `HashMap` functions requires manually looking up `TypeId`s. +/// let mut map = TypeIdMap::default(); +/// map.insert(TypeId::of::(), 7); +/// assert_eq!(map.get(&TypeId::of::()), Some(&7)); +/// +/// // Using `TypeIdMapExt` functions does the lookup for you. +/// map.insert_type::(7); +/// assert_eq!(map.get_type::(), Some(&7)); +/// ``` +pub trait TypeIdMapExt { + /// Inserts a value for the type `T`. + /// + /// If the map did not previously contain this key then [`None`] is returned, + /// otherwise the value for this key is updated and the old value returned. + fn insert_type(&mut self, v: V) -> Option; + + /// Returns a reference to the value for type `T`, if one exists. + fn get_type(&self) -> Option<&V>; + + /// Returns a mutable reference to the value for type `T`, if one exists. + fn get_type_mut(&mut self) -> Option<&mut V>; + + /// Removes type `T` from the map, returning the value for this + /// key if it was previously present. + fn remove_type(&mut self) -> Option; + + /// Gets the type `T`'s entry in the map for in-place manipulation. + fn entry_type(&mut self) -> Entry<'_, TypeId, V, NoOpHash>; +} + +impl TypeIdMapExt for TypeIdMap { + #[inline] + fn insert_type(&mut self, v: V) -> Option { + self.insert(TypeId::of::(), v) + } + + #[inline] + fn get_type(&self) -> Option<&V> { + self.get(&TypeId::of::()) + } + + #[inline] + fn get_type_mut(&mut self) -> Option<&mut V> { + self.get_mut(&TypeId::of::()) + } + + #[inline] + fn remove_type(&mut self) -> Option { + self.remove(&TypeId::of::()) + } + + #[inline] + fn entry_type(&mut self) -> Entry<'_, TypeId, V, NoOpHash> { + self.entry(TypeId::of::()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -67,7 +139,7 @@ mod tests { #[test] fn stable_hash_within_same_program_execution() { use alloc::vec::Vec; - + let mut map_1 = >::default(); let mut map_2 = >::default(); for i in 1..10 { diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 2e3e7edf18..1a5bca6916 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_window" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides windowing functionality for Bevy Engine" homepage = "https://bevy.org" @@ -50,16 +50,16 @@ libm = ["bevy_math/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", "smol_str", ], optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1.0", features = [ diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 026b85dc32..89f219d269 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,5 +1,8 @@ use alloc::string::String; -use bevy_ecs::{entity::Entity, event::Event}; +use bevy_ecs::{ + entity::Entity, + event::{BufferedEvent, Event}, +}; use bevy_input::{ gestures::*, keyboard::{KeyboardFocusLost, KeyboardInput}, @@ -23,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use crate::WindowTheme; /// A window event that is sent whenever a window's logical size has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -45,7 +48,7 @@ pub struct WindowResized { /// An event that indicates all of the application's windows should be redrawn, /// even if their control flow is set to `Wait` and there have been no window events. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -61,7 +64,7 @@ pub struct RequestRedraw; /// An event that is sent whenever a new window is created. /// /// To create a new window, spawn an entity with a [`crate::Window`] on it. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -87,7 +90,7 @@ pub struct WindowCreated { /// /// [`WindowPlugin`]: crate::WindowPlugin /// [`Window`]: crate::Window -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -105,7 +108,7 @@ pub struct WindowCloseRequested { /// An event that is sent whenever a window is closed. This will be sent when /// the window entity loses its [`Window`](crate::window::Window) component or is despawned. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -126,7 +129,7 @@ pub struct WindowClosed { /// An event that is sent whenever a window is closing. This will be sent when /// after a [`WindowCloseRequested`] event is received and the window is in the process of closing. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -146,7 +149,7 @@ pub struct WindowClosing { /// /// Note that if your application only has a single window, this event may be your last chance to /// persist state before the application terminates. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -176,7 +179,7 @@ pub struct WindowDestroyed { /// you should not use it for non-cursor-like behavior such as 3D camera control. Please see `MouseMotion` instead. /// /// [`WindowEvent::CursorMoved`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.CursorMoved -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -201,7 +204,7 @@ pub struct CursorMoved { } /// An event that is sent whenever the user's cursor enters a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -218,7 +221,7 @@ pub struct CursorEntered { } /// An event that is sent whenever the user's cursor leaves a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -239,7 +242,7 @@ pub struct CursorLeft { /// This event is the translated version of the `WindowEvent::Ime` from the `winit` crate. /// /// It is only sent if IME was enabled on the window with [`Window::ime_enabled`](crate::window::Window::ime_enabled). -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -284,7 +287,7 @@ pub enum Ime { } /// An event that indicates a window has received or lost focus. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -311,7 +314,7 @@ pub struct WindowFocused { /// It is the translated version of [`WindowEvent::Occluded`] from the `winit` crate. /// /// [`WindowEvent::Occluded`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.Occluded -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -330,7 +333,7 @@ pub struct WindowOccluded { } /// An event that indicates a window's scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -349,7 +352,7 @@ pub struct WindowScaleFactorChanged { } /// An event that indicates a window's OS-reported scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -368,7 +371,7 @@ pub struct WindowBackendScaleFactorChanged { } /// Events related to files being dragged and dropped on a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -404,7 +407,7 @@ pub enum FileDragAndDrop { } /// An event that is sent when a window is repositioned in physical pixels. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -426,7 +429,7 @@ pub struct WindowMoved { /// /// This event is only sent when the window is relying on the system theme to control its appearance. /// i.e. It is only sent when [`Window::window_theme`](crate::window::Window::window_theme) is `None` and the system theme changes. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -445,7 +448,7 @@ pub struct WindowThemeChanged { } /// Application lifetime events -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -488,7 +491,7 @@ impl AppLifecycle { /// access window events in the order they were received from the /// operating system. Otherwise, the event types are individually /// readable with `EventReader` (e.g. `EventReader`). -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -499,38 +502,66 @@ impl AppLifecycle { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] pub enum WindowEvent { + /// An application lifecycle event. AppLifecycle(AppLifecycle), + /// The user's cursor has entered a window. CursorEntered(CursorEntered), + ///The user's cursor has left a window. CursorLeft(CursorLeft), + /// The user's cursor has moved inside a window. CursorMoved(CursorMoved), + /// A file drag and drop event. FileDragAndDrop(FileDragAndDrop), + /// An Input Method Editor event. Ime(Ime), + /// A redraw of all of the application's windows has been requested. RequestRedraw(RequestRedraw), + /// The window's OS-reported scale factor has changed. WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), + /// The OS has requested that a window be closed. WindowCloseRequested(WindowCloseRequested), + /// A new window has been created. WindowCreated(WindowCreated), + /// A window has been destroyed by the underlying windowing system. WindowDestroyed(WindowDestroyed), + /// A window has received or lost focus. WindowFocused(WindowFocused), + /// A window has been moved. WindowMoved(WindowMoved), + /// A window has started or stopped being occluded. WindowOccluded(WindowOccluded), + /// A window's logical size has changed. WindowResized(WindowResized), + /// A window's scale factor has changed. WindowScaleFactorChanged(WindowScaleFactorChanged), + /// Sent for windows that are using the system theme when the system theme changes. WindowThemeChanged(WindowThemeChanged), + /// The state of a mouse button has changed. MouseButtonInput(MouseButtonInput), + /// The physical position of a pointing device has changed. MouseMotion(MouseMotion), + /// The mouse wheel has moved. MouseWheel(MouseWheel), + /// A two finger pinch gesture. PinchGesture(PinchGesture), + /// A two finger rotation gesture. RotationGesture(RotationGesture), + /// A double tap gesture. DoubleTapGesture(DoubleTapGesture), + /// A pan gesture. PanGesture(PanGesture), + /// A touch input state change. TouchInput(TouchInput), + /// A keyboard input. KeyboardInput(KeyboardInput), + /// Sent when focus has been lost for all Bevy windows. + /// + /// Used to clear pressed key state. KeyboardFocusLost(KeyboardFocusLost), } @@ -539,131 +570,157 @@ impl From for WindowEvent { Self::AppLifecycle(e) } } + impl From for WindowEvent { fn from(e: CursorEntered) -> Self { Self::CursorEntered(e) } } + impl From for WindowEvent { fn from(e: CursorLeft) -> Self { Self::CursorLeft(e) } } + impl From for WindowEvent { fn from(e: CursorMoved) -> Self { Self::CursorMoved(e) } } + impl From for WindowEvent { fn from(e: FileDragAndDrop) -> Self { Self::FileDragAndDrop(e) } } + impl From for WindowEvent { fn from(e: Ime) -> Self { Self::Ime(e) } } + impl From for WindowEvent { fn from(e: RequestRedraw) -> Self { Self::RequestRedraw(e) } } + impl From for WindowEvent { fn from(e: WindowBackendScaleFactorChanged) -> Self { Self::WindowBackendScaleFactorChanged(e) } } + impl From for WindowEvent { fn from(e: WindowCloseRequested) -> Self { Self::WindowCloseRequested(e) } } + impl From for WindowEvent { fn from(e: WindowCreated) -> Self { Self::WindowCreated(e) } } + impl From for WindowEvent { fn from(e: WindowDestroyed) -> Self { Self::WindowDestroyed(e) } } + impl From for WindowEvent { fn from(e: WindowFocused) -> Self { Self::WindowFocused(e) } } + impl From for WindowEvent { fn from(e: WindowMoved) -> Self { Self::WindowMoved(e) } } + impl From for WindowEvent { fn from(e: WindowOccluded) -> Self { Self::WindowOccluded(e) } } + impl From for WindowEvent { fn from(e: WindowResized) -> Self { Self::WindowResized(e) } } + impl From for WindowEvent { fn from(e: WindowScaleFactorChanged) -> Self { Self::WindowScaleFactorChanged(e) } } + impl From for WindowEvent { fn from(e: WindowThemeChanged) -> Self { Self::WindowThemeChanged(e) } } + impl From for WindowEvent { fn from(e: MouseButtonInput) -> Self { Self::MouseButtonInput(e) } } + impl From for WindowEvent { fn from(e: MouseMotion) -> Self { Self::MouseMotion(e) } } + impl From for WindowEvent { fn from(e: MouseWheel) -> Self { Self::MouseWheel(e) } } + impl From for WindowEvent { fn from(e: PinchGesture) -> Self { Self::PinchGesture(e) } } + impl From for WindowEvent { fn from(e: RotationGesture) -> Self { Self::RotationGesture(e) } } + impl From for WindowEvent { fn from(e: DoubleTapGesture) -> Self { Self::DoubleTapGesture(e) } } + impl From for WindowEvent { fn from(e: PanGesture) -> Self { Self::PanGesture(e) } } + impl From for WindowEvent { fn from(e: TouchInput) -> Self { Self::TouchInput(e) } } + impl From for WindowEvent { fn from(e: KeyboardInput) -> Self { Self::KeyboardInput(e) } } + impl From for WindowEvent { fn from(e: KeyboardFocusLost) -> Self { Self::KeyboardFocusLost(e) diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index fb8f1fb18f..22e657cf03 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -57,6 +57,7 @@ impl Default for WindowPlugin { fn default() -> Self { WindowPlugin { primary_window: Some(Window::default()), + primary_cursor_options: Some(CursorOptions::default()), exit_condition: ExitCondition::OnAllClosed, close_when_requested: true, } @@ -76,6 +77,13 @@ pub struct WindowPlugin { /// [`exit_on_all_closed`]. pub primary_window: Option, + /// Settings for the cursor on the primary window. + /// + /// Defaults to `Some(CursorOptions::default())`. + /// + /// Has no effect if [`WindowPlugin::primary_window`] is `None`. + pub primary_cursor_options: Option, + /// Whether to exit the app when there are no open windows. /// /// If disabling this, ensure that you send the [`bevy_app::AppExit`] @@ -122,10 +130,14 @@ impl Plugin for WindowPlugin { .add_event::(); if let Some(primary_window) = &self.primary_window { - app.world_mut().spawn(primary_window.clone()).insert(( + let mut entity_commands = app.world_mut().spawn(primary_window.clone()); + entity_commands.insert(( PrimaryWindow, RawHandleWrapperHolder(Arc::new(Mutex::new(None))), )); + if let Some(primary_cursor_options) = &self.primary_cursor_options { + entity_commands.insert(primary_cursor_options.clone()); + } } match self.exit_condition { @@ -168,7 +180,8 @@ impl Plugin for WindowPlugin { // Register window descriptor and related types #[cfg(feature = "bevy_reflect")] app.register_type::() - .register_type::(); + .register_type::() + .register_type::(); } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 4f7d7b2f7b..6f61eae2de 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -158,10 +158,8 @@ impl ContainsEntity for NormalizedWindowRef { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] +#[require(CursorOptions)] pub struct Window { - /// The cursor options of this window. Cursor icons are set with the `Cursor` component on the - /// window entity. - pub cursor_options: CursorOptions, /// What presentation mode to give the window. pub present_mode: PresentMode, /// Which fullscreen or windowing mode should be used. @@ -470,7 +468,6 @@ impl Default for Window { Self { title: DEFAULT_WINDOW_TITLE.to_owned(), name: None, - cursor_options: Default::default(), present_mode: Default::default(), mode: Default::default(), position: Default::default(), @@ -706,15 +703,13 @@ impl WindowResizeConstraints { min_height = min_height.max(1.); if max_width < min_width { warn!( - "The given maximum width {} is smaller than the minimum width {}", - max_width, min_width + "The given maximum width {max_width} is smaller than the minimum width {min_width}" ); max_width = min_width; } if max_height < min_height { warn!( - "The given maximum height {} is smaller than the minimum height {}", - max_height, min_height + "The given maximum height {max_height} is smaller than the minimum height {min_height}", ); max_height = min_height; } @@ -728,11 +723,11 @@ impl WindowResizeConstraints { } /// Cursor data for a [`Window`]. -#[derive(Debug, Clone)] +#[derive(Component, Debug, Clone)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), - reflect(Debug, Default, Clone) + reflect(Component, Debug, Default, Clone) )] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -754,11 +749,10 @@ pub struct CursorOptions { /// /// ## Platform-specific /// - /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] /// - **`iOS/Android`** don't have cursors. /// - /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. + /// Since `macOS` doesn't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. pub grab_mode: CursorGrabMode, /// Set whether or not mouse events within *this* window are captured or fall through to the Window below. @@ -1067,11 +1061,10 @@ impl From for WindowResolution { /// /// ## Platform-specific /// -/// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] /// - **`iOS/Android`** don't have cursors. /// -/// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. +/// Since `macOS` doesn't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", @@ -1171,13 +1164,17 @@ pub enum MonitorSelection { /// References an exclusive fullscreen video mode. /// /// Used when setting [`WindowMode::Fullscreen`] on a window. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Clone)] pub enum VideoModeSelection { /// Uses the video mode that the monitor is already in. Current, diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 43db87a1d2..43dcc0506b 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_winit" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A winit window and input backend for Bevy Engine" homepage = "https://bevy.org" @@ -28,25 +28,25 @@ custom_cursor = ["bevy_image", "bevy_asset", "bytemuck", "wgpu-types"] [dependencies] # bevy -bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } # bevy optional -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev", optional = true } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev", optional = true } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", optional = true } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev", optional = true } # other # feature rwh_06 refers to window_raw_handle@v0.6 @@ -59,7 +59,7 @@ cfg-if = "1.0" raw-window-handle = "0.6" serde = { version = "1.0", features = ["derive"], optional = true } bytemuck = { version = "1.5", optional = true } -wgpu-types = { version = "24", optional = true } +wgpu-types = { version = "25", optional = true } accesskit = "0.19" tracing = { version = "0.1", default-features = false, features = ["std"] } @@ -68,16 +68,16 @@ wasm-bindgen = { version = "0.2" } web-sys = "0.3" crossbeam-channel = "0.5" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index e3e4cb9f87..c5c5e489a6 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -22,8 +22,8 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - lifecycle::OnRemove, - observer::Trigger, + lifecycle::Remove, + observer::On, query::With, reflect::ReflectComponent, system::{Commands, Local, Query}, @@ -192,10 +192,10 @@ fn update_cursors( } /// Resets the cursor to the default icon when `CursorIcon` is removed. -fn on_remove_cursor_icon(trigger: Trigger, mut commands: Commands) { +fn on_remove_cursor_icon(trigger: On, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands - .entity(trigger.target().unwrap()) + .entity(trigger.target()) .try_insert(PendingCursor(Some(CursorSource::System( convert_system_cursor_icon(SystemCursorIcon::Default), )))); diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 968386bc02..8926095dc0 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -25,8 +25,8 @@ use winit::{event_loop::EventLoop, window::WindowId}; use bevy_a11y::AccessibilityRequested; use bevy_app::{App, Last, Plugin}; use bevy_ecs::prelude::*; -use bevy_window::{exit_on_all_closed, Window, WindowCreated}; -use system::{changed_windows, check_keyboard_focus_lost, despawn_windows}; +use bevy_window::{exit_on_all_closed, CursorOptions, Window, WindowCreated}; +use system::{changed_cursor_options, changed_windows, check_keyboard_focus_lost, despawn_windows}; pub use system::{create_monitors, create_windows}; #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub use winit::platform::web::CustomCursorExtWebSys; @@ -55,7 +55,9 @@ mod winit_monitors; mod winit_windows; thread_local! { - static WINIT_WINDOWS: RefCell = const { RefCell::new(WinitWindows::new()) }; + /// Temporary storage of WinitWindows data to replace usage of `!Send` resources. This will be replaced with proper + /// storage of `!Send` data after issue #17667 is complete. + pub static WINIT_WINDOWS: RefCell = const { RefCell::new(WinitWindows::new()) }; } /// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input @@ -69,9 +71,9 @@ thread_local! { /// in systems. /// /// When using eg. `MinimalPlugins` you can add this using `WinitPlugin::::default()`, where -/// `WakeUp` is the default `Event` that bevy uses. +/// `WakeUp` is the default event that bevy uses. #[derive(Default)] -pub struct WinitPlugin { +pub struct WinitPlugin { /// Allows the window (and the event loop) to be created on any thread /// instead of only the main thread. /// @@ -85,7 +87,7 @@ pub struct WinitPlugin { marker: PhantomData, } -impl Plugin for WinitPlugin { +impl Plugin for WinitPlugin { fn name(&self) -> &str { "bevy_winit::WinitPlugin" } @@ -140,6 +142,7 @@ impl Plugin for WinitPlugin { // `exit_on_all_closed` only checks if windows exist but doesn't access data, // so we don't need to care about its ordering relative to `changed_windows` changed_windows.ambiguous_with(exit_on_all_closed), + changed_cursor_options, despawn_windows, check_keyboard_focus_lost, ) @@ -153,7 +156,7 @@ impl Plugin for WinitPlugin { /// The default event that can be used to wake the window loop /// Wakes up the loop if in wait state -#[derive(Debug, Default, Clone, Copy, Event, Reflect)] +#[derive(Debug, Default, Clone, Copy, Event, BufferedEvent, Reflect)] #[reflect(Debug, Default, Clone)] pub struct WakeUp; @@ -164,7 +167,7 @@ pub struct WakeUp; /// /// When you receive this event it has already been handled by Bevy's main loop. /// Sending these events will NOT cause them to be processed by Bevy. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] pub struct RawWinitWindowEvent { /// The window for which the event was fired. pub window_id: WindowId, @@ -209,6 +212,7 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( ( Entity, &'static mut Window, + &'static CursorOptions, Option<&'static RawHandleWrapperHolder>, ), F, diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 083341fd2b..5b873d4620 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -46,7 +46,7 @@ use bevy_window::{ WindowScaleFactorChanged, WindowThemeChanged, }; #[cfg(target_os = "android")] -use bevy_window::{PrimaryWindow, RawHandleWrapper}; +use bevy_window::{CursorOptions, PrimaryWindow, RawHandleWrapper}; use crate::{ accessibility::ACCESS_KIT_ADAPTERS, @@ -58,7 +58,7 @@ use crate::{ /// Persistent state that is used to run the [`App`] according to the current /// [`UpdateMode`]. -struct WinitAppRunnerState { +struct WinitAppRunnerState { /// The running app. app: App, /// Exit value once the loop is finished. @@ -106,7 +106,7 @@ struct WinitAppRunnerState { )>, } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn new(mut app: App) -> Self { app.add_event::(); #[cfg(feature = "custom_cursor")] @@ -198,7 +198,7 @@ pub enum CursorSource { #[derive(Component, Debug)] pub struct PendingCursor(pub Option); -impl ApplicationHandler for WinitAppRunnerState { +impl ApplicationHandler for WinitAppRunnerState { fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { if event_loop.exiting() { return; @@ -474,7 +474,7 @@ impl ApplicationHandler for WinitAppRunnerState { if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) { if window_component.is_changed() { - cache.window = window_component.clone(); + **cache = window_component.clone(); } } }); @@ -549,7 +549,7 @@ impl ApplicationHandler for WinitAppRunnerState { } } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn redraw_requested(&mut self, event_loop: &ActiveEventLoop) { let mut redraw_event_reader = EventCursor::::default(); @@ -605,10 +605,12 @@ impl WinitAppRunnerState { { // Get windows that are cached but without raw handles. Those window were already created, but got their // handle wrapper removed when the app was suspended. + let mut query = self.world_mut() - .query_filtered::<(Entity, &Window), (With, Without)>(); - if let Ok((entity, window)) = query.single(&self.world()) { + .query_filtered::<(Entity, &Window, &CursorOptions), (With, Without)>(); + if let Ok((entity, window, cursor_options)) = query.single(&self.world()) { let window = window.clone(); + let cursor_options = cursor_options.clone(); WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { @@ -622,6 +624,7 @@ impl WinitAppRunnerState { event_loop, entity, &window, + &cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -934,7 +937,7 @@ impl WinitAppRunnerState { /// /// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the /// `EventLoop`. -pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { +pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { if app.plugins_state() == PluginsState::Ready { app.finish(); app.cleanup(); diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 873949ea89..6d3a76c9b3 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ + change_detection::DetectChangesMut, entity::Entity, event::EventWriter, lifecycle::RemovedComponents, @@ -10,9 +12,9 @@ use bevy_ecs::{ }; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use bevy_window::{ - ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, - WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, WindowResized, - WindowWrapper, + ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, + WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, + WindowResized, WindowWrapper, }; use tracing::{error, info, warn}; @@ -59,7 +61,7 @@ pub fn create_windows( ) { WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { - for (entity, mut window, handle_holder) in &mut created_windows { + for (entity, mut window, cursor_options, handle_holder) in &mut created_windows { if winit_windows.get_window(entity).is_some() { continue; } @@ -70,6 +72,7 @@ pub fn create_windows( event_loop, entity, &window, + cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -85,9 +88,8 @@ pub fn create_windows( .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); commands.entity(entity).insert(( - CachedWindow { - window: window.clone(), - }, + CachedWindow(window.clone()), + CachedCursorOptions(cursor_options.clone()), WinitWindowPressedKeys::default(), )); @@ -281,10 +283,12 @@ pub(crate) fn despawn_windows( } /// The cached state of the window so we can check which properties were changed from within the app. -#[derive(Debug, Clone, Component)] -pub struct CachedWindow { - pub window: Window, -} +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedWindow(Window); + +/// The cached state of the window so we can check which properties were changed from within the app. +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedCursorOptions(CursorOptions); /// Propagates changes from [`Window`] entities to the [`winit`] backend. /// @@ -306,11 +310,11 @@ pub(crate) fn changed_windows( continue; }; - if window.title != cache.window.title { + if window.title != cache.title { winit_window.set_title(window.title.as_str()); } - if window.mode != cache.window.mode { + if window.mode != cache.mode { let new_mode = match window.mode { WindowMode::BorderlessFullscreen(monitor_selection) => { Some(Some(winit::window::Fullscreen::Borderless(select_monitor( @@ -328,7 +332,7 @@ pub(crate) fn changed_windows( &monitor_selection, ) .unwrap_or_else(|| { - panic!("Could not find monitor for {:?}", monitor_selection) + panic!("Could not find monitor for {monitor_selection:?}") }); if let Some(video_mode) = get_selected_videomode(monitor, &video_mode_selection) @@ -352,15 +356,15 @@ pub(crate) fn changed_windows( } } - if window.resolution != cache.window.resolution { + if window.resolution != cache.resolution { let mut physical_size = PhysicalSize::new( window.resolution.physical_width(), window.resolution.physical_height(), ); let cached_physical_size = PhysicalSize::new( - cache.window.physical_width(), - cache.window.physical_height(), + cache.physical_width(), + cache.physical_height(), ); let base_scale_factor = window.resolution.base_scale_factor(); @@ -368,12 +372,12 @@ pub(crate) fn changed_windows( // Note: this may be different from `winit`'s base scale factor if // `scale_factor_override` is set to Some(f32) let scale_factor = window.scale_factor(); - let cached_scale_factor = cache.window.scale_factor(); + let cached_scale_factor = cache.scale_factor(); // Check and update `winit`'s physical size only if the window is not maximized if scale_factor != cached_scale_factor && !winit_window.is_maximized() { let logical_size = - if let Some(cached_factor) = cache.window.resolution.scale_factor_override() { + if let Some(cached_factor) = cache.resolution.scale_factor_override() { physical_size.to_logical::(cached_factor as f64) } else { physical_size.to_logical::(base_scale_factor as f64) @@ -397,7 +401,7 @@ pub(crate) fn changed_windows( } } - if window.physical_cursor_position() != cache.window.physical_cursor_position() { + if window.physical_cursor_position() != cache.physical_cursor_position() { if let Some(physical_position) = window.physical_cursor_position() { let position = PhysicalPosition::new(physical_position.x, physical_position.y); @@ -407,44 +411,23 @@ pub(crate) fn changed_windows( } } - if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode - && crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode) - .is_err() - { - window.cursor_options.grab_mode = cache.window.cursor_options.grab_mode; - } - - if window.cursor_options.visible != cache.window.cursor_options.visible { - winit_window.set_cursor_visible(window.cursor_options.visible); - } - - if window.cursor_options.hit_test != cache.window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { - window.cursor_options.hit_test = cache.window.cursor_options.hit_test; - warn!( - "Could not set cursor hit test for window {}: {}", - window.title, err - ); - } - } - - if window.decorations != cache.window.decorations + if window.decorations != cache.decorations && window.decorations != winit_window.is_decorated() { winit_window.set_decorations(window.decorations); } - if window.resizable != cache.window.resizable + if window.resizable != cache.resizable && window.resizable != winit_window.is_resizable() { winit_window.set_resizable(window.resizable); } - if window.enabled_buttons != cache.window.enabled_buttons { + if window.enabled_buttons != cache.enabled_buttons { winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); } - if window.resize_constraints != cache.window.resize_constraints { + if window.resize_constraints != cache.resize_constraints { let constraints = window.resize_constraints.check_constraints(); let min_inner_size = LogicalSize { width: constraints.min_width, @@ -461,7 +444,7 @@ pub(crate) fn changed_windows( } } - if window.position != cache.window.position { + if window.position != cache.position { if let Some(position) = crate::winit_window_position( &window.position, &window.resolution, @@ -502,62 +485,62 @@ pub(crate) fn changed_windows( } } - if window.focused != cache.window.focused && window.focused { + if window.focused != cache.focused && window.focused { winit_window.focus_window(); } - if window.window_level != cache.window.window_level { + if window.window_level != cache.window_level { winit_window.set_window_level(convert_window_level(window.window_level)); } // Currently unsupported changes - if window.transparent != cache.window.transparent { - window.transparent = cache.window.transparent; + if window.transparent != cache.transparent { + window.transparent = cache.transparent; warn!("Winit does not currently support updating transparency after window creation."); } #[cfg(target_arch = "wasm32")] - if window.canvas != cache.window.canvas { - window.canvas.clone_from(&cache.window.canvas); + if window.canvas != cache.canvas { + window.canvas.clone_from(&cache.canvas); warn!( "Bevy currently doesn't support modifying the window canvas after initialization." ); } - if window.ime_enabled != cache.window.ime_enabled { + if window.ime_enabled != cache.ime_enabled { winit_window.set_ime_allowed(window.ime_enabled); } - if window.ime_position != cache.window.ime_position { + if window.ime_position != cache.ime_position { winit_window.set_ime_cursor_area( LogicalPosition::new(window.ime_position.x, window.ime_position.y), PhysicalSize::new(10, 10), ); } - if window.window_theme != cache.window.window_theme { + if window.window_theme != cache.window_theme { winit_window.set_theme(window.window_theme.map(convert_window_theme)); } - if window.visible != cache.window.visible { + if window.visible != cache.visible { winit_window.set_visible(window.visible); } #[cfg(target_os = "ios")] { - if window.recognize_pinch_gesture != cache.window.recognize_pinch_gesture { + if window.recognize_pinch_gesture != cache.recognize_pinch_gesture { winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); } - if window.recognize_rotation_gesture != cache.window.recognize_rotation_gesture { + if window.recognize_rotation_gesture != cache.recognize_rotation_gesture { winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); } - if window.recognize_doubletap_gesture != cache.window.recognize_doubletap_gesture { + if window.recognize_doubletap_gesture != cache.recognize_doubletap_gesture { winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); } - if window.recognize_pan_gesture != cache.window.recognize_pan_gesture { + if window.recognize_pan_gesture != cache.recognize_pan_gesture { match ( window.recognize_pan_gesture, - cache.window.recognize_pan_gesture, + cache.recognize_pan_gesture, ) { (Some(_), Some(_)) => { warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers"); @@ -567,16 +550,15 @@ pub(crate) fn changed_windows( } } - if window.prefers_home_indicator_hidden != cache.window.prefers_home_indicator_hidden { + if window.prefers_home_indicator_hidden != cache.prefers_home_indicator_hidden { winit_window .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); } - if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { + if window.prefers_status_bar_hidden != cache.prefers_status_bar_hidden { winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); } if window.preferred_screen_edges_deferring_system_gestures != cache - .window .preferred_screen_edges_deferring_system_gestures { use crate::converters::convert_screen_edge; @@ -585,7 +567,59 @@ pub(crate) fn changed_windows( winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge); } } - cache.window = window.clone(); + **cache = window.clone(); + } + }); +} + +pub(crate) fn changed_cursor_options( + mut changed_windows: Query< + ( + Entity, + &Window, + &mut CursorOptions, + &mut CachedCursorOptions, + ), + Changed, + >, + _non_send_marker: NonSendMarker, +) { + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, window, mut cursor_options, mut cache) in &mut changed_windows { + // This system already only runs when the cursor options change, so we need to bypass change detection or the next frame will also run this system + let cursor_options = cursor_options.bypass_change_detection(); + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + // Don't check the cache for the grab mode. It can change through external means, leaving the cache outdated. + if let Err(err) = + crate::winit_windows::attempt_grab(winit_window, cursor_options.grab_mode) + { + warn!( + "Could not set cursor grab mode for window {}: {}", + window.title, err + ); + cursor_options.grab_mode = cache.grab_mode; + } else { + cache.grab_mode = cursor_options.grab_mode; + } + + if cursor_options.visible != cache.visible { + winit_window.set_cursor_visible(cursor_options.visible); + cache.visible = cursor_options.visible; + } + + if cursor_options.hit_test != cache.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { + warn!( + "Could not set cursor hit test for window {}: {}", + window.title, err + ); + cursor_options.hit_test = cache.hit_test; + } else { + cache.hit_test = cursor_options.hit_test; + } + } } }); } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 8bf326f453..f238da2e20 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -4,8 +4,8 @@ use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; use bevy_platform::collections::HashMap; use bevy_window::{ - CursorGrabMode, MonitorSelection, VideoModeSelection, Window, WindowMode, WindowPosition, - WindowResolution, WindowWrapper, + CursorGrabMode, CursorOptions, MonitorSelection, VideoModeSelection, Window, WindowMode, + WindowPosition, WindowResolution, WindowWrapper, }; use tracing::warn; @@ -58,6 +58,7 @@ impl WinitWindows { event_loop: &ActiveEventLoop, entity: Entity, window: &Window, + cursor_options: &CursorOptions, adapters: &mut AccessKitAdapters, handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, @@ -310,16 +311,16 @@ impl WinitWindows { winit_window.set_visible(window.visible); // Do not set the grab mode on window creation if it's none. It can fail on mobile. - if window.cursor_options.grab_mode != CursorGrabMode::None { - let _ = attempt_grab(&winit_window, window.cursor_options.grab_mode); + if cursor_options.grab_mode != CursorGrabMode::None { + let _ = attempt_grab(&winit_window, cursor_options.grab_mode); } - winit_window.set_cursor_visible(window.cursor_options.visible); + winit_window.set_cursor_visible(cursor_options.visible); // Do not set the cursor hittest on window creation if it's false, as it will always fail on // some platforms and log an unfixable warning. - if !window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { + if !cursor_options.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { warn!( "Could not set cursor hit test for window {}: {}", window.title, err @@ -528,7 +529,7 @@ impl core::fmt::Display for DisplayInfo { let millihertz = self.refresh_rate_millihertz.unwrap_or(0); let hertz = millihertz / 1000; let extra_millihertz = millihertz % 1000; - write!(f, " Refresh rate (Hz): {}.{:03}", hertz, extra_millihertz)?; + write!(f, " Refresh rate (Hz): {hertz}.{extra_millihertz:03}")?; Ok(()) } } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index e0f00f2f3d..20e6e53698 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -21,6 +21,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_audio|Provides audio functionality| |bevy_color|Provides shared color types and operations| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_core_widgets|Headless widget collection for Bevy UI.| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| @@ -40,6 +41,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_window|Windowing layer| |bevy_winit|winit window and input backend| |custom_cursor|Enable winit custom cursor support| +|debug|Enable collecting debug information about systems and components to help with diagnostics| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |hdr|HDR image format support| |ktx2|KTX2 compressed texture support| @@ -68,8 +70,10 @@ The default feature set enables most of the expected features of a game engine, |bevy_dev_tools|Provides a collection of developer tools| |bevy_image|Load and access image data. Usually added by an image format| |bevy_remote|Enable the Bevy Remote Protocol| +|bevy_solari|Provides raytraced lighting (experimental)| |bevy_ui_debug|Provides a debug overlay for bevy UI| |bmp|BMP image format support| +|compressed_image_saver|Enables compressed KTX2 UASTC texture output on the asset processor| |critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.| |dds|DDS compressed texture support| |debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam| diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index 9d9be1e5c7..9fb8791ed5 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -190,7 +190,7 @@ fn update_bloom_settings( } } - text.push_str(&format!("(O) Tonemapping: {:?}\n", tonemapping)); + text.push_str(&format!("(O) Tonemapping: {tonemapping:?}\n")); if keycode.just_pressed(KeyCode::KeyO) { commands .entity(camera_entity) diff --git a/examples/2d/tilemap_chunk.rs b/examples/2d/tilemap_chunk.rs new file mode 100644 index 0000000000..8663b036b1 --- /dev/null +++ b/examples/2d/tilemap_chunk.rs @@ -0,0 +1,81 @@ +//! Shows a tilemap chunk rendered with a single draw call. + +use bevy::{ + prelude::*, + sprite::{TilemapChunk, TilemapChunkIndices}, +}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) + .add_systems(Startup, setup) + .add_systems(Update, (update_tileset_image, update_tilemap)) + .run(); +} + +#[derive(Component, Deref, DerefMut)] +struct UpdateTimer(Timer); + +#[derive(Resource, Deref, DerefMut)] +struct SeededRng(ChaCha8Rng); + +fn setup(mut commands: Commands, assets: Res) { + // We're seeding the PRNG here to make this example deterministic for testing purposes. + // This isn't strictly required in practical use unless you need your app to be deterministic. + let mut rng = ChaCha8Rng::seed_from_u64(42); + + let chunk_size = UVec2::splat(64); + let tile_display_size = UVec2::splat(8); + let indices: Vec> = (0..chunk_size.element_product()) + .map(|_| rng.gen_range(0..5)) + .map(|i| if i == 0 { None } else { Some(i - 1) }) + .collect(); + + commands.spawn(( + TilemapChunk { + chunk_size, + tile_display_size, + tileset: assets.load("textures/array_texture.png"), + ..default() + }, + TilemapChunkIndices(indices), + UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + )); + + commands.spawn(Camera2d); + + commands.insert_resource(SeededRng(rng)); +} + +fn update_tileset_image( + chunk_query: Single<&TilemapChunk>, + mut events: EventReader>, + mut images: ResMut>, +) { + let chunk = *chunk_query; + for event in events.read() { + if event.is_loaded_with_dependencies(chunk.tileset.id()) { + let image = images.get_mut(&chunk.tileset).unwrap(); + image.reinterpret_stacked_2d_as_array(4); + } + } +} + +fn update_tilemap( + time: Res, mut b: EventReade } /// A dummy event type. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] struct DebugEvent { resend_from_param_set: bool, resend_from_local_event_reader: bool, diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 0a0b04f7a2..b8d02e8159 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -89,7 +89,7 @@ struct Ball; #[derive(Component, Deref, DerefMut)] struct Velocity(Vec2); -#[derive(Event, Default)] +#[derive(Event, BufferedEvent, Default)] struct CollisionEvent; #[derive(Component)] diff --git a/examples/games/desk_toy.rs b/examples/games/desk_toy.rs index c25286dd9c..b5c638348e 100644 --- a/examples/games/desk_toy.rs +++ b/examples/games/desk_toy.rs @@ -10,7 +10,7 @@ use bevy::{ app::AppExit, input::common_conditions::{input_just_pressed, input_just_released}, prelude::*, - window::{PrimaryWindow, WindowLevel}, + window::{CursorOptions, PrimaryWindow, WindowLevel}, }; #[cfg(target_os = "macos")] @@ -219,12 +219,13 @@ fn get_cursor_world_pos( /// Update whether the window is clickable or not fn update_cursor_hit_test( cursor_world_pos: Res, - mut primary_window: Single<&mut Window, With>, + primary_window: Single<(&Window, &mut CursorOptions), With>, bevy_logo_transform: Single<&Transform, With>, ) { + let (window, mut cursor_options) = primary_window.into_inner(); // If the window has decorations (e.g. a border) then it should be clickable - if primary_window.decorations { - primary_window.cursor_options.hit_test = true; + if window.decorations { + cursor_options.hit_test = true; return; } @@ -234,7 +235,7 @@ fn update_cursor_hit_test( }; // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable - primary_window.cursor_options.hit_test = bevy_logo_transform + cursor_options.hit_test = bevy_logo_transform .translation .truncate() .distance(cursor_world_pos) diff --git a/examples/games/stepping.rs b/examples/games/stepping.rs index 653b7a12ff..dce37d0842 100644 --- a/examples/games/stepping.rs +++ b/examples/games/stepping.rs @@ -104,7 +104,10 @@ fn build_ui( mut state: ResMut, ) { let mut text_spans = Vec::new(); - let mut always_run = Vec::new(); + let mut always_run: Vec<( + bevy_ecs::intern::Interned, + NodeId, + )> = Vec::new(); let Ok(schedule_order) = stepping.schedules() else { return; @@ -131,7 +134,8 @@ fn build_ui( for (node_id, system) in systems { // skip bevy default systems; we don't want to step those - if system.name().starts_with("bevy") { + #[cfg(feature = "debug")] + if system.name().as_string().starts_with("bevy") { always_run.push((*label, node_id)); continue; } diff --git a/examples/helpers/camera_controller.rs b/examples/helpers/camera_controller.rs index a60aa69a5e..3f6e4ed477 100644 --- a/examples/helpers/camera_controller.rs +++ b/examples/helpers/camera_controller.rs @@ -8,7 +8,7 @@ use bevy::{ input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit}, prelude::*, - window::CursorGrabMode, + window::{CursorGrabMode, CursorOptions}, }; use std::{f32::consts::*, fmt}; @@ -126,7 +126,7 @@ Freecam Controls: fn run_camera_controller( time: Res