diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c4f6e2bd0f..92ea9273f1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://bevyengine.org/donate/ +custom: https://bevy.org/donate/ diff --git a/.github/ISSUE_TEMPLATE/docs_improvement.md b/.github/ISSUE_TEMPLATE/docs_improvement.md index 4bc84c5fc9..f4b6f2019e 100644 --- a/.github/ISSUE_TEMPLATE/docs_improvement.md +++ b/.github/ISSUE_TEMPLATE/docs_improvement.md @@ -10,4 +10,4 @@ assignees: '' Provide a link to the documentation and describe how it could be improved. In what ways is it incomplete, incorrect, or misleading? -If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevyengine.org/learn/contribute/helping-out/writing-docs/) instead. +If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevy.org/learn/contribute/helping-out/writing-docs/) instead. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a0610cf03..8ae7f6597e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,9 +13,9 @@ env: CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0 # If nightly is breaking CI, modify this variable to target a specific nightly version. - NIGHTLY_TOOLCHAIN: nightly-2025-05-16 # pinned until a fix for https://github.com/rust-lang/miri/issues/4323 is released + NIGHTLY_TOOLCHAIN: nightly RUSTFLAGS: "-D warnings" - BINSTALL_VERSION: "v1.12.3" + BINSTALL_VERSION: "v1.14.1" 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 @@ -260,7 +260,7 @@ jobs: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 - name: Run Markdown Lint - uses: super-linter/super-linter/slim@v7.3.0 + uses: super-linter/super-linter/slim@v7.4.0 env: MULTI_STATUS: false VALIDATE_ALL_CODEBASE: false @@ -272,7 +272,8 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: cargo-bins/cargo-binstall@v1.12.3 + # Update in sync with BINSTALL_VERSION + - uses: cargo-bins/cargo-binstall@v1.14.1 - name: Install taplo run: cargo binstall taplo-cli@0.9.3 --locked - name: Run Taplo @@ -293,7 +294,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.32.0 + uses: crate-ci/typos@v1.34.0 - name: Typos info if: failure() run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a04fadc94..e732cbe66d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -82,7 +82,7 @@ jobs: - name: Finalize documentation run: | echo "" > target/doc/index.html - echo "dev-docs.bevyengine.org" > target/doc/CNAME + echo "dev-docs.bevy.org" > target/doc/CNAME echo $'User-Agent: *\nDisallow: /' > target/doc/robots.txt rm target/doc/.lock 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/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 96a287981d..87df34b932 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -43,5 +43,5 @@ jobs: repo: context.repo.repo, body: `**Welcome**, new contributor! - Please make sure you've read our [contributing guide](https://bevyengine.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` + Please make sure you've read our [contributing guide](https://bevy.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` }) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73bc77c455..887318f22a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ # Contributing to Bevy If you'd like to help build Bevy, start by reading this -[introduction](https://bevyengine.org/learn/contribute/introduction). Thanks for contributing! +[introduction](https://bevy.org/learn/contribute/introduction). Thanks for contributing! diff --git a/Cargo.toml b/Cargo.toml index a3d3a2ab63..f047040bdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,16 @@ [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" exclude = ["assets/", "tools/", ".github/", "crates/", "examples/wasm/assets/"] -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" keywords = ["game", "engine", "gamedev", "graphics", "bevy"] license = "MIT OR Apache-2.0" repository = "https://github.com/bevyengine/bevy" documentation = "https://docs.rs/bevy" -rust-version = "1.85.0" +rust-version = "1.88.0" [workspace] resolver = "2" @@ -133,6 +133,7 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_core_widgets", "bevy_anti_aliasing", "bevy_gilrs", "bevy_gizmos", @@ -150,6 +151,7 @@ default = [ "bevy_text", "bevy_ui", "bevy_ui_picking_backend", + "bevy_ui_render", "bevy_window", "bevy_winit", "custom_cursor", @@ -163,6 +165,8 @@ default = [ "vorbis", "webgl2", "x11", + "debug", + "zstd_rust", ] # Recommended defaults for no_std applications @@ -245,6 +249,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", @@ -267,6 +280,9 @@ bevy_ui = [ "bevy_anti_aliasing", ] +# Provides rendering functionality for bevy_ui +bevy_ui_render = ["bevy_internal/bevy_ui_render", "bevy_render", "bevy_ui"] + # Windowing layer bevy_window = ["bevy_internal/bevy_window"] @@ -291,8 +307,11 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] -# Use the configurable global error handler as the default error handler. -configurable_error_handler = ["bevy_internal/configurable_error_handler"] +# Headless widget collection for Bevy UI. +bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] + +# Feathers widget collection. +experimental_bevy_feathers = ["bevy_internal/bevy_feathers"] # 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"] @@ -319,6 +338,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"] @@ -367,8 +389,11 @@ webp = ["bevy_internal/webp"] # For KTX2 supercompression zlib = ["bevy_internal/zlib"] -# For KTX2 supercompression -zstd = ["bevy_internal/zstd"] +# For KTX2 Zstandard decompression using pure rust [ruzstd](https://crates.io/crates/ruzstd). This is the safe default. For maximum performance, use "zstd_c". +zstd_rust = ["bevy_internal/zstd_rust"] + +# For KTX2 Zstandard decompression using [zstd](https://crates.io/crates/zstd). This is a faster backend, but uses unsafe C bindings. For the safe option, stick to the default backend with "zstd_rust". +zstd_c = ["bevy_internal/zstd_c"] # FLAC audio format support flac = ["bevy_internal/flac"] @@ -437,7 +462,7 @@ android_shared_stdcxx = ["bevy_internal/android_shared_stdcxx"] detailed_trace = ["bevy_internal/detailed_trace"] # Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method for your `Camera2d` or `Camera3d`. -tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "zstd"] +tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "bevy_image/zstd"] # Include SMAA Look Up Tables KTX2 Files smaa_luts = ["bevy_internal/smaa_luts"] @@ -466,6 +491,12 @@ shader_format_wesl = ["bevy_internal/shader_format_wesl"] # Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs pbr_transmission_textures = ["bevy_internal/pbr_transmission_textures"] +# Enable support for Clustered Decals +pbr_clustered_decals = ["bevy_internal/pbr_clustered_decals"] + +# Enable support for Light Textures +pbr_light_textures = ["bevy_internal/pbr_light_textures"] + # Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs pbr_multi_layer_material_textures = [ "bevy_internal/pbr_multi_layer_material_textures", @@ -496,7 +527,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"] @@ -537,30 +571,41 @@ libm = ["bevy_internal/libm"] # Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures. web = ["bevy_internal/web"] +# Enable hotpatching of Bevy systems +hotpatching = ["bevy_internal/hotpatching"] + +# Enable converting glTF coordinates to Bevy's coordinate system by default. This will be Bevy's default behavior starting in 0.18. +gltf_convert_coordinates_default = [ + "bevy_internal/gltf_convert_coordinates_default", +] + +# 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" -bytemuck = "1.7" -bevy_render = { path = "crates/bevy_render", version = "0.16.0-dev", default-features = false } +serde_json = "1.0.140" +bytemuck = "1" +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" @@ -572,7 +617,7 @@ hyper = { version = "1", features = ["server", "http1"] } http-body-util = "0.1" anyhow = "1" macro_rules_attribute = "0.2" -accesskit = "0.18" +accesskit = "0.19" nonmax = "0.5" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] @@ -585,6 +630,17 @@ ureq = { version = "3.0.8", features = ["json"] } wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = ["Window"] } +[[example]] +name = "context_menu" +path = "examples/usage/context_menu.rs" +doc-scrape-examples = true + +[package.metadata.example.context_menu] +name = "Context Menu" +description = "Example of a context menu" +category = "Usage" +wasm = true + [[example]] name = "hello_world" path = "examples/hello_world.rs" @@ -812,8 +868,20 @@ doc-scrape-examples = true name = "Texture Atlas" description = "Generates a texture atlas (sprite sheet) from individual sprites" category = "2D Rendering" +# Loading asset folders is not supported in Wasm, but required to create the atlas. 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" @@ -878,6 +946,7 @@ doc-scrape-examples = true name = "2D Wireframe" description = "Showcases wireframes for 2d meshes" category = "2D Rendering" +# PolygonMode::Line wireframes are not supported by WebGL wasm = false # 3D Rendering @@ -945,6 +1014,7 @@ doc-scrape-examples = true name = "Anti-aliasing" description = "Compares different anti-aliasing methods" category = "3D Rendering" +# TAA not supported by WebGL wasm = false [[example]] @@ -989,6 +1059,7 @@ doc-scrape-examples = true name = "Auto Exposure" description = "A scene showcasing auto exposure" category = "3D Rendering" +# Requires compute shaders, which are not supported by WebGL. wasm = false [[example]] @@ -1002,6 +1073,17 @@ description = "Showcases different blend modes" category = "3D Rendering" wasm = true +[[example]] +name = "manual_material" +path = "examples/3d/manual_material.rs" +doc-scrape-examples = true + +[package.metadata.example.manual_material] +name = "Manual Material Implementation" +description = "Demonstrates how to implement a material manually using the mid-level render APIs" +category = "3D Rendering" +wasm = true + [[example]] name = "edit_material_on_gltf" path = "examples/3d/edit_material_on_gltf.rs" @@ -1045,6 +1127,7 @@ doc-scrape-examples = true name = "Screen Space Ambient Occlusion" description = "A scene showcasing screen space ambient occlusion" category = "3D Rendering" +# Requires compute shaders, which are not supported by WebGL. wasm = false [[example]] @@ -1144,6 +1227,7 @@ doc-scrape-examples = true name = "Order Independent Transparency" description = "Demonstrates how to use OIT" category = "3D Rendering" +# Not supported by WebGL wasm = false [[example]] @@ -1243,7 +1327,19 @@ doc-scrape-examples = true name = "Skybox" description = "Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats." category = "3D Rendering" -wasm = false +wasm = true + +[[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" @@ -1342,6 +1438,7 @@ doc-scrape-examples = true name = "Wireframe" description = "Showcases wireframe rendering" category = "3D Rendering" +# Not supported on WebGL wasm = false [[example]] @@ -1353,6 +1450,8 @@ doc-scrape-examples = true name = "Irradiance Volumes" description = "Demonstrates irradiance volumes" category = "3D Rendering" +# On WebGL and WebGPU, the number of texture bindings is too low +# See wasm = false [[example]] @@ -1365,6 +1464,7 @@ required-features = ["meshlet"] name = "Meshlet" description = "Meshlet rendering for dense high-poly scenes (experimental)" category = "3D Rendering" +# Requires compute shaders and WGPU extensions, not supported by WebGL nor WebGPU. wasm = false setup = [ [ @@ -1400,7 +1500,7 @@ doc-scrape-examples = true name = "Lightmaps" description = "Rendering a scene with baked lightmaps" category = "3D Rendering" -wasm = false +wasm = true [[example]] name = "no_prepass" @@ -1553,6 +1653,7 @@ doc-scrape-examples = true name = "Custom Loop" description = "Demonstrates how to create a custom runner (to update an app manually)" category = "Application" +# Doesn't render anything, doesn't create a canvas wasm = false [[example]] @@ -1564,6 +1665,7 @@ doc-scrape-examples = true name = "Drag and Drop" description = "An example that shows how to handle drag and drop in an app" category = "Application" +# Browser drag and drop is not supported wasm = false [[example]] @@ -1575,6 +1677,7 @@ doc-scrape-examples = true name = "Empty" description = "An empty application (does nothing)" category = "Application" +# Doesn't render anything, doesn't create a canvas wasm = false [[example]] @@ -1598,6 +1701,7 @@ required-features = ["bevy_log"] name = "Headless" description = "An application that runs without default plugins" category = "Application" +# Doesn't render anything, doesn't create a canvas wasm = false [[example]] @@ -1620,6 +1724,8 @@ doc-scrape-examples = true name = "Log layers" description = "Illustrate how to add custom log layers" category = "Application" +# Accesses `time`, which is not available on the web +# Also doesn't render anything wasm = false [[example]] @@ -1631,6 +1737,7 @@ doc-scrape-examples = true name = "Advanced log layers" description = "Illustrate how to transfer data between log layers and Bevy's ECS" category = "Application" +# Doesn't render anything, doesn't create a canvas wasm = false [[example]] @@ -2006,12 +2113,6 @@ description = "Full guide to Bevy's ECS" category = "ECS (Entity Component System)" wasm = false -[package.metadata.example.apply_deferred] -name = "Apply System Buffers" -description = "Show how to use `ApplyDeferred` system" -category = "ECS (Entity Component System)" -wasm = false - [[example]] name = "change_detection" path = "examples/ecs/change_detection.rs" @@ -2061,6 +2162,7 @@ wasm = false name = "dynamic" path = "examples/ecs/dynamic.rs" doc-scrape-examples = true +required-features = ["debug"] [package.metadata.example.dynamic] name = "Dynamic ECS" @@ -2215,7 +2317,6 @@ wasm = false name = "fallible_params" path = "examples/ecs/fallible_params.rs" doc-scrape-examples = true -required-features = ["configurable_error_handler"] [package.metadata.example.fallible_params] name = "Fallible System Parameters" @@ -2227,7 +2328,7 @@ wasm = false name = "error_handling" path = "examples/ecs/error_handling.rs" doc-scrape-examples = true -required-features = ["bevy_mesh_picking_backend", "configurable_error_handler"] +required-features = ["bevy_mesh_picking_backend"] [package.metadata.example.error_handling] name = "Error handling" @@ -3422,6 +3523,28 @@ description = "An example for CSS Grid layout" category = "UI (User Interface)" wasm = true +[[example]] +name = "gradients" +path = "examples/ui/gradients.rs" +doc-scrape-examples = true + +[package.metadata.example.gradients] +name = "Gradients" +description = "An example demonstrating gradients" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "stacked_gradients" +path = "examples/ui/stacked_gradients.rs" +doc-scrape-examples = true + +[package.metadata.example.stacked_gradients] +name = "Stacked Gradients" +description = "An example demonstrating stacked gradients" +category = "UI (User Interface)" +wasm = true + [[example]] name = "scroll" path = "examples/ui/scroll.rs" @@ -3510,6 +3633,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_transform" +path = "examples/ui/ui_transform.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_transform] +name = "UI Transform" +description = "An example demonstrating how to translate, rotate and scale UI elements." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" @@ -3893,6 +4027,16 @@ description = "A simple 2D screen shake effect" category = "Camera" wasm = true +[[example]] +name = "2d_on_ui" +path = "examples/camera/2d_on_ui.rs" +doc-scrape-examples = true + +[package.metadata.example.2d_on_ui] +name = "2D on Bevy UI" +description = "Shows how to render 2D objects on top of Bevy UI" +category = "Camera" +wasm = true [package.metadata.example.fps_overlay] name = "FPS overlay" @@ -4198,6 +4342,14 @@ description = "Demonstrates specular tints and maps" category = "3D Rendering" wasm = true +[[example]] +name = "test_invalid_skinned_mesh" +path = "tests/3d/test_invalid_skinned_mesh.rs" +doc-scrape-examples = true + +[package.metadata.example.test_invalid_skinned_mesh] +hidden = true + [profile.wasm-release] inherits = "release" opt-level = "z" @@ -4297,6 +4449,7 @@ wasm = true name = "clustered_decals" path = "examples/3d/clustered_decals.rs" doc-scrape-examples = true +required-features = ["pbr_clustered_decals"] [package.metadata.example.clustered_decals] name = "Clustered Decals" @@ -4304,6 +4457,18 @@ description = "Demonstrates clustered decals" category = "3D Rendering" wasm = false +[[example]] +name = "light_textures" +path = "examples/3d/light_textures.rs" +doc-scrape-examples = true +required-features = ["pbr_light_textures"] + +[package.metadata.example.light_textures] +name = "Light Textures" +description = "Demonstrates light textures" +category = "3D Rendering" +wasm = false + [[example]] name = "occlusion_culling" path = "examples/3d/occlusion_culling.rs" @@ -4361,3 +4526,72 @@ name = "Extended Bindless Material" description = "Demonstrates bindless `ExtendedMaterial`" category = "Shaders" wasm = false + +[[example]] +name = "cooldown" +path = "examples/usage/cooldown.rs" +doc-scrape-examples = true + +[package.metadata.example.cooldown] +name = "Cooldown" +description = "Example for cooldown on button clicks" +category = "Usage" +wasm = true + +[[example]] +name = "hotpatching_systems" +path = "examples/ecs/hotpatching_systems.rs" +doc-scrape-examples = true +required-features = ["hotpatching"] + +[package.metadata.example.hotpatching_systems] +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 + +[[example]] +name = "scrollbars" +path = "examples/ui/scrollbars.rs" +doc-scrape-examples = true + +[package.metadata.example.scrollbars] +name = "Scrollbars" +description = "Demonstrates use of core scrollbar in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "feathers" +path = "examples/ui/feathers.rs" +doc-scrape-examples = true +required-features = ["experimental_bevy_feathers"] + +[package.metadata.example.feathers] +name = "Feathers Widgets" +description = "Gallery of Feathers Widgets" +category = "UI (User Interface)" +wasm = true +hidden = true diff --git a/README.md b/README.md index be1bcf6bfe..db0a3d57b1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevyengine.org) +# [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevy.org) [![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.svg)](https://crates.io/crates/bevy) @@ -13,7 +13,7 @@ Bevy is a refreshingly simple data-driven game engine built in Rust. It is free ## WARNING -Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevyengine.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevyengine.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment. +Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevy.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevy.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment. **MSRV:** Bevy relies heavily on improvements in the Rust language and compiler. As a result, the Minimum Supported Rust Version (MSRV) is generally close to "the latest stable release" of Rust. @@ -29,15 +29,15 @@ As a result, the Minimum Supported Rust Version (MSRV) is generally close to "th ## About -* **[Features](https://bevyengine.org):** A quick overview of Bevy's features. -* **[News](https://bevyengine.org/news/)**: A development blog that covers our progress, plans and shiny new features. +* **[Features](https://bevy.org):** A quick overview of Bevy's features. +* **[News](https://bevy.org/news/)**: A development blog that covers our progress, plans and shiny new features. ## Docs -* **[Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy. +* **[Quick Start Guide](https://bevy.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy. * **[Bevy Rust API Docs](https://docs.rs/bevy):** Bevy's Rust API docs, which are automatically generated from the doc comments in this repo. * **[Official Examples](https://github.com/bevyengine/bevy/tree/latest/examples):** Bevy's dedicated, runnable examples, which are great for digging into specific concepts. -* **[Community-Made Learning Resources](https://bevyengine.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community. +* **[Community-Made Learning Resources](https://bevy.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community. ## Community @@ -46,11 +46,11 @@ Before contributing or participating in discussions with the community, you shou * **[Discord](https://discord.gg/bevy):** Bevy's official discord server. * **[Reddit](https://reddit.com/r/bevy):** Bevy's official subreddit. * **[GitHub Discussions](https://github.com/bevyengine/bevy/discussions):** The best place for questions about Bevy, answered right here! -* **[Bevy Assets](https://bevyengine.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials. +* **[Bevy Assets](https://bevy.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials. ### Contributing -If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevyengine.org/learn/contribute/introduction)**. +If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevy.org/learn/contribute/introduction)**. For simple problems, feel free to [open an issue](https://github.com/bevyengine/bevy/issues) or [PR](https://github.com/bevyengine/bevy/pulls) and tackle it yourself! @@ -58,9 +58,9 @@ For more complex architecture decisions and experimental mad science, please ope ## Getting Started -We recommend checking out the [Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction) for a brief introduction. +We recommend checking out the [Quick Start Guide](https://bevy.org/learn/quick-start/introduction) for a brief introduction. -Follow the [Setup guide](https://bevyengine.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly. +Follow the [Setup guide](https://bevy.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly. Once set up, you can quickly try out the [examples](https://github.com/bevyengine/bevy/tree/latest/examples) by cloning this repo and running the following commands: ```sh @@ -75,7 +75,7 @@ To draw a window with standard functionality enabled, use: ```rust use bevy::prelude::*; -fn main(){ +fn main() { App::new() .add_plugins(DefaultPlugins) .run(); @@ -84,7 +84,7 @@ fn main(){ ### Fast Compiles -Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevyengine.org/learn/quick-start/getting-started/setup). +Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevy.org/learn/quick-start/getting-started/setup). ## [Bevy Cargo Features][cargo_features] @@ -96,7 +96,7 @@ This [list][cargo_features] outlines the different cargo features supported by B Bevy is the result of the hard work of many people. A huge thanks to all Bevy contributors, the many open source projects that have come before us, the [Rust gamedev ecosystem](https://arewegameyet.rs/), and the many libraries we build on. -A huge thanks to Bevy's [generous sponsors](https://bevyengine.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevyengine.org/donate/) if you like what we're building. +A huge thanks to Bevy's [generous sponsors](https://bevy.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevy.org/donate/) if you like what we're building. This project is tested with BrowserStack. diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron index e9d6f4f9cf..b91869b118 100644 --- a/assets/animation_graphs/Fox.animgraph.ron +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -9,20 +9,20 @@ ( node_type: Blend, mask: 0, - weight: 1.0, + weight: 0.5, ), ( - node_type: Clip(AssetPath("models/animated/Fox.glb#Animation0")), + node_type: Clip("models/animated/Fox.glb#Animation0"), mask: 0, weight: 1.0, ), ( - node_type: Clip(AssetPath("models/animated/Fox.glb#Animation1")), + node_type: Clip("models/animated/Fox.glb#Animation1"), mask: 0, weight: 1.0, ), ( - node_type: Clip(AssetPath("models/animated/Fox.glb#Animation2")), + node_type: Clip("models/animated/Fox.glb#Animation2"), mask: 0, weight: 1.0, ), 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/lightmaps/caustic_directional_texture.png b/assets/lightmaps/caustic_directional_texture.png new file mode 100644 index 0000000000..0c082a46cf Binary files /dev/null and b/assets/lightmaps/caustic_directional_texture.png differ diff --git a/assets/lightmaps/faces_pointlight_texture_blurred.png b/assets/lightmaps/faces_pointlight_texture_blurred.png new file mode 100644 index 0000000000..1adf2e954d Binary files /dev/null and b/assets/lightmaps/faces_pointlight_texture_blurred.png differ diff --git a/assets/lightmaps/torch_spotlight_texture.png b/assets/lightmaps/torch_spotlight_texture.png new file mode 100644 index 0000000000..23a7b39719 Binary files /dev/null and b/assets/lightmaps/torch_spotlight_texture.png differ diff --git a/assets/models/Faces/faces.glb b/assets/models/Faces/faces.glb new file mode 100644 index 0000000000..23cbb2f7c0 Binary files /dev/null and b/assets/models/Faces/faces.glb differ 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..cb650c8a47 100644 --- a/assets/shaders/custom_vertex_attribute.wgsl +++ b/assets/shaders/custom_vertex_attribute.wgsl @@ -1,9 +1,11 @@ +// For 2d replace `bevy_pbr::mesh_functions` with `bevy_sprite::mesh2d_functions` +// and `mesh_position_local_to_clip` with `mesh2d_position_local_to_clip`. #import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip} 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..ae89f7dfea 100644 --- a/assets/shaders/extended_material.wgsl +++ b/assets/shaders/extended_material.wgsl @@ -17,9 +17,15 @@ struct MyExtendedMaterial { quantize_steps: u32, +#ifdef SIXTEEN_BYTE_ALIGNMENT + // Web examples WebGL2 support: structs must be 16 byte aligned. + _webgl2_padding_8b: u32, + _webgl2_padding_12b: u32, + _webgl2_padding_16b: u32, +#endif } -@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/manual_material.wgsl b/assets/shaders/manual_material.wgsl new file mode 100644 index 0000000000..557a1412c7 --- /dev/null +++ b/assets/shaders/manual_material.wgsl @@ -0,0 +1,11 @@ +#import bevy_pbr::forward_io::VertexOutput + +@group(3) @binding(0) var material_color_texture: texture_2d; +@group(3) @binding(1) var material_color_sampler: sampler; + +@fragment +fn fragment( + mesh: VertexOutput, +) -> @location(0) vec4 { + return textureSample(material_color_texture, material_color_sampler, mesh.uv); +} 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/assets/textures/food_kenney.png b/assets/textures/food_kenney.png new file mode 100644 index 0000000000..a5fe374eba Binary files /dev/null and b/assets/textures/food_kenney.png differ diff --git a/benches/Cargo.toml b/benches/Cargo.toml index a299a6526c..789207d823 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -10,7 +10,7 @@ autobenches = false [dependencies] # The primary crate that runs and analyzes our benchmarks. This is a regular dependency because the # `bench!` macro refers to it in its documentation. -criterion = { version = "0.5.1", features = ["html_reports"] } +criterion = { version = "0.6.0", features = ["html_reports"] } [dev-dependencies] # Bevy crates diff --git a/benches/benches/bevy_ecs/bundles/insert_many.rs b/benches/benches/bevy_ecs/bundles/insert_many.rs new file mode 100644 index 0000000000..2e9bfbd8b0 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/insert_many.rs @@ -0,0 +1,67 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 2_000; + +#[derive(Component)] +struct C(usize); + +pub fn insert_many(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("insert_many")); + + group.bench_function("all", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world + .spawn_empty() + .insert(C::<0>(1)) + .insert(C::<1>(1)) + .insert(C::<2>(1)) + .insert(C::<3>(1)) + .insert(C::<4>(1)) + .insert(C::<5>(1)) + .insert(C::<6>(1)) + .insert(C::<7>(1)) + .insert(C::<8>(1)) + .insert(C::<9>(1)) + .insert(C::<10>(1)) + .insert(C::<11>(1)) + .insert(C::<12>(1)) + .insert(C::<13>(1)) + .insert(C::<14>(1)); + } + world.clear_entities(); + }); + }); + + group.bench_function("only_last", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world + .spawn(( + C::<0>(1), + C::<1>(1), + C::<2>(1), + C::<3>(1), + C::<4>(1), + C::<5>(1), + C::<6>(1), + C::<7>(1), + C::<8>(1), + C::<9>(1), + C::<10>(1), + C::<11>(1), + C::<12>(1), + C::<13>(1), + )) + .insert(C::<14>(1)); + } + world.clear_entities(); + }); + }); + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/bundles/mod.rs b/benches/benches/bevy_ecs/bundles/mod.rs new file mode 100644 index 0000000000..21ef05eb3b --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/mod.rs @@ -0,0 +1,14 @@ +use criterion::criterion_group; + +mod insert_many; +mod spawn_many; +mod spawn_many_zst; +mod spawn_one_zst; + +criterion_group!( + benches, + spawn_one_zst::spawn_one_zst, + spawn_many_zst::spawn_many_zst, + spawn_many::spawn_many, + insert_many::insert_many, +); diff --git a/benches/benches/bevy_ecs/bundles/spawn_many.rs b/benches/benches/bevy_ecs/bundles/spawn_many.rs new file mode 100644 index 0000000000..bd99cf3c56 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/spawn_many.rs @@ -0,0 +1,40 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 2_000; + +#[derive(Component)] +struct C(usize); + +pub fn spawn_many(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("spawn_many")); + + group.bench_function("static", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world.spawn(( + C::<0>(1), + C::<1>(1), + C::<2>(1), + C::<3>(1), + C::<4>(1), + C::<5>(1), + C::<6>(1), + C::<7>(1), + C::<8>(1), + C::<9>(1), + C::<10>(1), + C::<11>(1), + C::<12>(1), + C::<13>(1), + C::<14>(1), + )); + } + world.clear_entities(); + }); + }); + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/bundles/spawn_many_zst.rs b/benches/benches/bevy_ecs/bundles/spawn_many_zst.rs new file mode 100644 index 0000000000..b4305f9e43 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/spawn_many_zst.rs @@ -0,0 +1,27 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 2_000; + +#[derive(Component)] +struct C; + +pub fn spawn_many_zst(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("spawn_many_zst")); + + group.bench_function("static", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world.spawn(( + C::<0>, C::<1>, C::<2>, C::<3>, C::<4>, C::<5>, C::<6>, C::<7>, C::<8>, C::<9>, + C::<10>, C::<11>, C::<12>, C::<13>, C::<14>, + )); + } + world.clear_entities(); + }); + }); + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/bundles/spawn_one_zst.rs b/benches/benches/bevy_ecs/bundles/spawn_one_zst.rs new file mode 100644 index 0000000000..acb006c1c7 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/spawn_one_zst.rs @@ -0,0 +1,24 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 10_000; + +#[derive(Component)] +struct A; + +pub fn spawn_one_zst(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("spawn_one_zst")); + + group.bench_function("static", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world.spawn(A); + } + world.clear_entities(); + }); + }); + + group.finish(); +} 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..5f85633312 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>); @@ -10,7 +10,7 @@ impl Benchmark { let mut events = Events::default(); for _ in 0..count { - events.send(BenchEvent([0u8; SIZE])); + events.write(BenchEvent([0u8; SIZE])); } Self(events) diff --git a/benches/benches/bevy_ecs/events/mod.rs b/benches/benches/bevy_ecs/events/mod.rs index b87a138e06..fcc807c968 100644 --- a/benches/benches/bevy_ecs/events/mod.rs +++ b/benches/benches/bevy_ecs/events/mod.rs @@ -1,5 +1,5 @@ mod iter; -mod send; +mod write; use criterion::{criterion_group, Criterion}; @@ -10,20 +10,20 @@ 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| { - let mut bench = send::Benchmark::<4>::new(count); + group.bench_function(format!("size_4_events_{count}"), |b| { + let mut bench = write::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| { - let mut bench = send::Benchmark::<16>::new(count); + group.bench_function(format!("size_16_events_{count}"), |b| { + let mut bench = write::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| { - let mut bench = send::Benchmark::<512>::new(count); + group.bench_function(format!("size_512_events_{count}"), |b| { + let mut bench = write::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/write.rs similarity index 82% rename from benches/benches/bevy_ecs/events/send.rs rename to benches/benches/bevy_ecs/events/write.rs index fa996b50aa..8095aee738 100644 --- a/benches/benches/bevy_ecs/events/send.rs +++ b/benches/benches/bevy_ecs/events/write.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct BenchEvent([u8; SIZE]); impl Default for BenchEvent { @@ -21,7 +21,7 @@ impl Benchmark { // Force both internal buffers to be allocated. for _ in 0..2 { for _ in 0..count { - events.send(BenchEvent([0u8; SIZE])); + events.write(BenchEvent([0u8; SIZE])); } events.update(); } @@ -32,7 +32,7 @@ impl Benchmark { pub fn run(&mut self) { for _ in 0..self.count { self.events - .send(core::hint::black_box(BenchEvent([0u8; SIZE]))); + .write(core::hint::black_box(BenchEvent([0u8; SIZE]))); } self.events.update(); } diff --git a/benches/benches/bevy_ecs/iteration/heavy_compute.rs b/benches/benches/bevy_ecs/iteration/heavy_compute.rs index 3a3e350637..e057b20a43 100644 --- a/benches/benches/bevy_ecs/iteration/heavy_compute.rs +++ b/benches/benches/bevy_ecs/iteration/heavy_compute.rs @@ -45,7 +45,6 @@ pub fn heavy_compute(c: &mut Criterion) { let mut system = IntoSystem::into_system(sys); system.initialize(&mut world); - system.update_archetype_component_access(world.as_unsafe_world_cell()); b.iter(move || system.run((), &mut world)); }); diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_system.rs b/benches/benches/bevy_ecs/iteration/iter_simple_system.rs index 2b6e828721..5b08bfd7e0 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_system.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_system.rs @@ -37,12 +37,11 @@ impl Benchmark { let mut system = IntoSystem::into_system(query_system); system.initialize(&mut world); - system.update_archetype_component_access(world.as_unsafe_world_cell()); Self(world, Box::new(system)) } #[inline(never)] pub fn run(&mut self) { - self.1.run((), &mut self.0); + self.1.run((), &mut self.0).unwrap(); } } 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/main.rs b/benches/benches/bevy_ecs/main.rs index 4a025ab829..59b4c1fd73 100644 --- a/benches/benches/bevy_ecs/main.rs +++ b/benches/benches/bevy_ecs/main.rs @@ -5,6 +5,7 @@ use criterion::criterion_main; +mod bundles; mod change_detection; mod components; mod empty_archetypes; @@ -18,6 +19,7 @@ mod scheduling; mod world; criterion_main!( + bundles::benches, change_detection::benches, components::benches, empty_archetypes::benches, 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 7b1cc29457..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(); @@ -62,6 +62,31 @@ pub fn spawn_commands(criterion: &mut Criterion) { group.finish(); } +pub fn nonempty_spawn_commands(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("nonempty_spawn_commands"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + for entity_count in [100, 1_000, 10_000] { + group.bench_function(format!("{entity_count}_entities"), |bencher| { + let mut world = World::default(); + let mut command_queue = CommandQueue::default(); + + bencher.iter(|| { + let mut commands = Commands::new(&mut command_queue, &world); + for i in 0..entity_count { + if black_box(i % 2 == 0) { + commands.spawn(A); + } + } + command_queue.apply(&mut world); + }); + }); + } + + group.finish(); +} + #[derive(Default, Component)] struct Matrix([[f32; 4]; 4]); @@ -137,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(); @@ -182,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/mod.rs b/benches/benches/bevy_ecs/world/mod.rs index e35dc999c2..7158f2f033 100644 --- a/benches/benches/bevy_ecs/world/mod.rs +++ b/benches/benches/bevy_ecs/world/mod.rs @@ -17,6 +17,7 @@ criterion_group!( benches, empty_commands, spawn_commands, + nonempty_spawn_commands, insert_commands, fake_commands, zero_sized_commands, 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_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index a95cb4a821..70f3cb6703 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -32,7 +32,7 @@ fn segment_ease(c: &mut Criterion) { fn curve_position(c: &mut Criterion) { /// A helper function that benchmarks calling [`CubicCurve::position()`] over a generic [`VectorSpace`]. - fn bench_curve( + fn bench_curve>( group: &mut BenchmarkGroup, name: &str, curve: CubicCurve

, 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/clippy.toml b/clippy.toml index 2c98e8ed02..372ffbaf0b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -41,7 +41,6 @@ disallowed-methods = [ { path = "f32::asinh", reason = "use bevy_math::ops::asinh instead for libm determinism" }, { path = "f32::acosh", reason = "use bevy_math::ops::acosh instead for libm determinism" }, { path = "f32::atanh", reason = "use bevy_math::ops::atanh instead for libm determinism" }, - { path = "criterion::black_box", reason = "use core::hint::black_box instead" }, ] # Require `bevy_ecs::children!` to use `[]` braces, instead of `()` or `{}`. diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 759cf3e787..70ee16cf77 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "accessibility", "a11y"] @@ -40,13 +40,13 @@ 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.18", default-features = false } +accesskit = { version = "0.19", default-features = false } serde = { version = "1", default-features = false, features = [ "alloc", ], optional = true } diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 94468c148c..22b2f71f07 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -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 11e819806c..731a6c7c4f 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -1,47 +1,46 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" 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_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" +petgraph = { version = "0.8", features = ["serde-1"] } +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"] } -smallvec = "1" +smallvec = { version = "1", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index 688011a32c..593ca04d2e 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -55,7 +55,7 @@ pub struct CubicKeyframeCurve { impl Curve for CubicKeyframeCurve where - V: VectorSpace, + V: VectorSpace, { #[inline] fn domain(&self) -> Interval { @@ -179,7 +179,7 @@ pub struct WideLinearKeyframeCurve { impl IterableCurve for WideLinearKeyframeCurve where - T: VectorSpace, + T: VectorSpace, { #[inline] fn domain(&self) -> Interval { @@ -289,7 +289,7 @@ pub struct WideCubicKeyframeCurve { impl IterableCurve for WideCubicKeyframeCurve where - T: VectorSpace, + T: VectorSpace, { #[inline] fn domain(&self) -> Interval { @@ -406,7 +406,7 @@ fn cubic_spline_interpolation( step_duration: f32, ) -> T where - T: VectorSpace, + T: VectorSpace, { let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp; value_start * (coeffs.x * lerp + 1.0) @@ -415,7 +415,7 @@ where + tangent_in_end * step_duration * lerp * coeffs.w } -fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( +fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( width: usize, first: &'a [T], second: &'a [T], diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aa6d252fee..adb4a7c7ac 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, @@ -18,7 +19,7 @@ use bevy_ecs::{ system::{Res, ResMut}, }; use bevy_platform::collections::HashMap; -use bevy_reflect::{prelude::ReflectDefault, Reflect, ReflectSerialize}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; use derive_more::derive::From; use petgraph::{ graph::{DiGraph, NodeIndex}, @@ -28,6 +29,7 @@ use ron::de::SpannedError; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use thiserror::Error; +use tracing::warn; use crate::{AnimationClip, AnimationTargetId}; @@ -107,9 +109,8 @@ use crate::{AnimationClip, AnimationTargetId}; /// [RON]: https://github.com/ron-rs/ron /// /// [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md -#[derive(Asset, Reflect, Clone, Debug, Serialize)] -#[reflect(Serialize, Debug, Clone)] -#[serde(into = "SerializedAnimationGraph")] +#[derive(Asset, Reflect, Clone, Debug)] +#[reflect(Debug, Clone)] pub struct AnimationGraph { /// The `petgraph` data structure that defines the animation graph. pub graph: AnimationDiGraph, @@ -241,20 +242,40 @@ pub enum AnimationNodeType { #[derive(Default)] pub struct AnimationGraphAssetLoader; -/// Various errors that can occur when serializing or deserializing animation -/// graphs to and from RON, respectively. +/// Errors that can occur when serializing animation graphs to RON. +#[derive(Error, Debug)] +pub enum AnimationGraphSaveError { + /// An I/O error occurred. + #[error(transparent)] + Io(#[from] io::Error), + /// An error occurred in RON serialization. + #[error(transparent)] + Ron(#[from] ron::Error), + /// An error occurred converting the graph to its serialization form. + #[error(transparent)] + ConvertToSerialized(#[from] NonPathHandleError), +} + +/// Errors that can occur when deserializing animation graphs from RON. #[derive(Error, Debug)] pub enum AnimationGraphLoadError { /// An I/O error occurred. - #[error("I/O")] + #[error(transparent)] Io(#[from] io::Error), - /// An error occurred in RON serialization or deserialization. - #[error("RON serialization")] + /// An error occurred in RON deserialization. + #[error(transparent)] Ron(#[from] ron::Error), /// An error occurred in RON deserialization, and the location of the error /// is supplied. - #[error("RON serialization")] + #[error(transparent)] SpannedRon(#[from] SpannedError), + /// The deserialized graph contained legacy data that we no longer support. + #[error( + "The deserialized AnimationGraph contained an AnimationClip referenced by an AssetId, \ + which is no longer supported. Consider manually deserializing the SerializedAnimationGraph \ + type and determine how to migrate any SerializedAnimationClip::AssetId animation clips" + )] + GraphContainsLegacyAssetId, } /// Acceleration structures for animation graphs that allows Bevy to evaluate @@ -387,18 +408,32 @@ pub struct SerializedAnimationGraphNode { #[derive(Serialize, Deserialize)] pub enum SerializedAnimationNodeType { /// Corresponds to [`AnimationNodeType::Clip`]. - Clip(SerializedAnimationClip), + Clip(MigrationSerializedAnimationClip), /// Corresponds to [`AnimationNodeType::Blend`]. Blend, /// Corresponds to [`AnimationNodeType::Add`]. Add, } -/// A version of `Handle` suitable for serializing as an asset. +/// A type to facilitate migration from the legacy format of [`SerializedAnimationGraph`] to the +/// new format. /// -/// This replaces any handle that has a path with an [`AssetPath`]. Failing -/// that, the asset ID is serialized directly. +/// By using untagged serde deserialization, we can try to deserialize the modern form, then +/// fallback to the legacy form. Users must migrate to the modern form by Bevy 0.18. +// TODO: Delete this after Bevy 0.17. #[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum MigrationSerializedAnimationClip { + /// This is the new type of this field. + Modern(AssetPath<'static>), + /// This is the legacy type of this field. Users must migrate away from this. + #[serde(skip_serializing)] + Legacy(SerializedAnimationClip), +} + +/// The legacy form of serialized animation clips. This allows raw asset IDs to be deserialized. +// TODO: Delete this after Bevy 0.17. +#[derive(Deserialize)] pub enum SerializedAnimationClip { /// Records an asset path. AssetPath(AssetPath<'static>), @@ -647,12 +682,13 @@ impl AnimationGraph { /// /// If writing to a file, it can later be loaded with the /// [`AnimationGraphAssetLoader`] to reconstruct the graph. - pub fn save(&self, writer: &mut W) -> Result<(), AnimationGraphLoadError> + pub fn save(&self, writer: &mut W) -> Result<(), AnimationGraphSaveError> where W: Write, { let mut ron_serializer = ron::ser::Serializer::new(writer, None)?; - Ok(self.serialize(&mut ron_serializer)?) + let serialized_graph: SerializedAnimationGraph = self.clone().try_into()?; + Ok(serialized_graph.serialize(&mut ron_serializer)?) } /// Adds an animation target (bone) to the mask group with the given ID. @@ -757,28 +793,55 @@ impl AssetLoader for AnimationGraphAssetLoader { let serialized_animation_graph = SerializedAnimationGraph::deserialize(&mut deserializer) .map_err(|err| deserializer.span_error(err))?; - // Load all `AssetPath`s to convert from a - // `SerializedAnimationGraph` to a real `AnimationGraph`. - Ok(AnimationGraph { - graph: serialized_animation_graph.graph.map( - |_, serialized_node| AnimationGraphNode { - node_type: match serialized_node.node_type { - SerializedAnimationNodeType::Clip(ref clip) => match clip { - SerializedAnimationClip::AssetId(asset_id) => { - AnimationNodeType::Clip(Handle::Weak(*asset_id)) + // Load all `AssetPath`s to convert from a `SerializedAnimationGraph` to a real + // `AnimationGraph`. This is effectively a `DiGraph::map`, but this allows us to return + // errors. + let mut animation_graph = DiGraph::with_capacity( + serialized_animation_graph.graph.node_count(), + serialized_animation_graph.graph.edge_count(), + ); + + let mut already_warned = false; + for serialized_node in serialized_animation_graph.graph.node_weights() { + animation_graph.add_node(AnimationGraphNode { + node_type: match serialized_node.node_type { + SerializedAnimationNodeType::Clip(ref clip) => match clip { + MigrationSerializedAnimationClip::Modern(path) => { + AnimationNodeType::Clip(load_context.load(path.clone())) + } + MigrationSerializedAnimationClip::Legacy( + SerializedAnimationClip::AssetPath(path), + ) => { + if !already_warned { + let path = load_context.asset_path(); + warn!( + "Loaded an AnimationGraph asset at \"{path}\" which contains a \ + legacy-style SerializedAnimationClip. Please re-save the asset \ + using AnimationGraph::save to automatically migrate to the new \ + format" + ); + already_warned = true; } - SerializedAnimationClip::AssetPath(asset_path) => { - AnimationNodeType::Clip(load_context.load(asset_path)) - } - }, - SerializedAnimationNodeType::Blend => AnimationNodeType::Blend, - SerializedAnimationNodeType::Add => AnimationNodeType::Add, + AnimationNodeType::Clip(load_context.load(path.clone())) + } + MigrationSerializedAnimationClip::Legacy( + SerializedAnimationClip::AssetId(_), + ) => { + return Err(AnimationGraphLoadError::GraphContainsLegacyAssetId); + } }, - mask: serialized_node.mask, - weight: serialized_node.weight, + SerializedAnimationNodeType::Blend => AnimationNodeType::Blend, + SerializedAnimationNodeType::Add => AnimationNodeType::Add, }, - |_, _| (), - ), + mask: serialized_node.mask, + weight: serialized_node.weight, + }); + } + for edge in serialized_animation_graph.graph.raw_edges() { + animation_graph.add_edge(edge.source(), edge.target(), ()); + } + Ok(AnimationGraph { + graph: animation_graph, root: serialized_animation_graph.root, mask_groups: serialized_animation_graph.mask_groups, }) @@ -789,37 +852,50 @@ impl AssetLoader for AnimationGraphAssetLoader { } } -impl From for SerializedAnimationGraph { - fn from(animation_graph: AnimationGraph) -> Self { - // If any of the animation clips have paths, then serialize them as - // `SerializedAnimationClip::AssetPath` so that the - // `AnimationGraphAssetLoader` can load them. - Self { - graph: animation_graph.graph.map( - |_, node| SerializedAnimationGraphNode { - weight: node.weight, - mask: node.mask, - node_type: match node.node_type { - AnimationNodeType::Clip(ref clip) => match clip.path() { - Some(path) => SerializedAnimationNodeType::Clip( - SerializedAnimationClip::AssetPath(path.clone()), - ), - None => SerializedAnimationNodeType::Clip( - SerializedAnimationClip::AssetId(clip.id()), - ), - }, - AnimationNodeType::Blend => SerializedAnimationNodeType::Blend, - AnimationNodeType::Add => SerializedAnimationNodeType::Add, +impl TryFrom for SerializedAnimationGraph { + type Error = NonPathHandleError; + + fn try_from(animation_graph: AnimationGraph) -> Result { + // Convert all the `Handle` to AssetPath, so that + // `AnimationGraphAssetLoader` can load them. This is effectively just doing a + // `DiGraph::map`, except we need to return an error if any handles aren't associated to a + // path. + let mut serialized_graph = DiGraph::with_capacity( + animation_graph.graph.node_count(), + animation_graph.graph.edge_count(), + ); + for node in animation_graph.graph.node_weights() { + serialized_graph.add_node(SerializedAnimationGraphNode { + weight: node.weight, + mask: node.mask, + node_type: match node.node_type { + AnimationNodeType::Clip(ref clip) => match clip.path() { + Some(path) => SerializedAnimationNodeType::Clip( + MigrationSerializedAnimationClip::Modern(path.clone()), + ), + None => return Err(NonPathHandleError), }, + AnimationNodeType::Blend => SerializedAnimationNodeType::Blend, + AnimationNodeType::Add => SerializedAnimationNodeType::Add, }, - |_, _| (), - ), + }); + } + for edge in animation_graph.graph.raw_edges() { + serialized_graph.add_edge(edge.source(), edge.target(), ()); + } + Ok(Self { + graph: serialized_graph, root: animation_graph.root, mask_groups: animation_graph.mask_groups, - } + }) } } +/// Error for when only path [`Handle`]s are supported. +#[derive(Error, Debug)] +#[error("AnimationGraph contains a handle to an AnimationClip that does not correspond to an asset path")] +pub struct NonPathHandleError; + /// A system that creates, updates, and removes [`ThreadedAnimationGraph`] /// structures for every changed [`AnimationGraph`]. /// diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 21ea15f96f..ae7ce42ed6 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Animation for the game engine Bevy @@ -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 5a8e48ecb5..8c32d70fff 100644 --- a/crates/bevy_anti_aliasing/Cargo.toml +++ b/crates/bevy_anti_aliasing/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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 707d75819d..872175dbc7 100644 --- a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs @@ -1,9 +1,9 @@ use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, 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 _; @@ -11,15 +11,16 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, prelude::Camera, - render_graph::RenderGraphApp, + render_graph::RenderGraphExt, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, *, }, renderer::RenderDevice, view::{ExtractedView, ViewTarget}, - Render, RenderApp, RenderSystems, + Render, RenderApp, RenderStartup, RenderSystems, }; +use bevy_utils::default; mod node; @@ -95,20 +96,12 @@ impl ExtractComponent for ContrastAdaptiveSharpening { } } -const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle = - weak_handle!("ef83f0a5-51df-4b51-9ab7-b5fd1ae5a397"); - /// Adds Support for Contrast Adaptive Sharpening (CAS). pub struct CasPlugin; impl Plugin for CasPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE, - "robust_contrast_adaptive_sharpening.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "robust_contrast_adaptive_sharpening.wgsl"); app.register_type::(); app.add_plugins(( @@ -121,6 +114,7 @@ impl Plugin for CasPlugin { }; render_app .init_resource::>() + .add_systems(RenderStartup, init_cas_pipeline) .add_systems(Render, prepare_cas_pipelines.in_set(RenderSystems::Prepare)); { @@ -158,44 +152,46 @@ impl Plugin for CasPlugin { ); } } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - render_app.init_resource::(); - } } #[derive(Resource)] pub struct CasPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } -impl FromWorld for CasPipeline { - fn from_world(render_world: &mut World) -> Self { - let render_device = render_world.resource::(); - let texture_bind_group = render_device.create_bind_group_layout( - "sharpening_texture_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: true }), - sampler(SamplerBindingType::Filtering), - // CAS Settings - uniform_buffer::(true), - ), +pub fn init_cas_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + let texture_bind_group = render_device.create_bind_group_layout( + "sharpening_texture_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + // CAS Settings + uniform_buffer::(true), ), - ); + ), + ); - let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); - CasPipeline { - texture_bind_group, - sampler, - } - } + commands.insert_resource(CasPipeline { + texture_bind_group, + sampler, + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!( + asset_server.as_ref(), + "robust_contrast_adaptive_sharpening.wgsl" + ), + }); } #[derive(PartialEq, Eq, Hash, Clone, Copy)] @@ -215,22 +211,18 @@ 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: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_anti_aliasing/src/experimental/mod.rs b/crates/bevy_anti_aliasing/src/experimental/mod.rs deleted file mode 100644 index a8dc522c56..0000000000 --- a/crates/bevy_anti_aliasing/src/experimental/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Experimental rendering features. -//! -//! Experimental features are features with known problems, missing features, -//! compatibility issues, low performance, and/or future breaking changes, but -//! are included nonetheless for testing purposes. - -pub mod taa { - pub use crate::taa::{TemporalAntiAliasNode, TemporalAntiAliasPlugin, TemporalAntiAliasing}; -} diff --git a/crates/bevy_anti_aliasing/src/fxaa/mod.rs b/crates/bevy_anti_aliasing/src/fxaa/mod.rs index 4848d3d268..bba5596263 100644 --- a/crates/bevy_anti_aliasing/src/fxaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/fxaa/mod.rs @@ -1,9 +1,9 @@ use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, 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 _; @@ -11,14 +11,14 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, prelude::Camera, - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, render_resource::{ binding_types::{sampler, texture_2d}, *, }, renderer::RenderDevice, view::{ExtractedView, ViewTarget}, - Render, RenderApp, RenderSystems, + Render, RenderApp, RenderStartup, RenderSystems, }; use bevy_utils::default; @@ -80,13 +80,11 @@ impl Default for Fxaa { } } -const FXAA_SHADER_HANDLE: Handle = weak_handle!("fc58c0a8-01c0-46e9-94cc-83a794bae7b0"); - /// Adds support for Fast Approximate Anti-Aliasing (FXAA) pub struct FxaaPlugin; impl Plugin for FxaaPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, FXAA_SHADER_HANDLE, "fxaa.wgsl", Shader::from_wgsl); + embedded_asset!(app, "fxaa.wgsl"); app.register_type::(); app.add_plugins(ExtractComponentPlugin::::default()); @@ -96,6 +94,7 @@ impl Plugin for FxaaPlugin { }; render_app .init_resource::>() + .add_systems(RenderStartup, init_fxaa_pipeline) .add_systems( Render, prepare_fxaa_pipelines.in_set(RenderSystems::Prepare), @@ -119,47 +118,46 @@ impl Plugin for FxaaPlugin { ), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - render_app.init_resource::(); - } } #[derive(Resource)] pub struct FxaaPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } -impl FromWorld for FxaaPipeline { - fn from_world(render_world: &mut World) -> Self { - let render_device = render_world.resource::(); - let texture_bind_group = render_device.create_bind_group_layout( - "fxaa_texture_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: true }), - sampler(SamplerBindingType::Filtering), - ), +pub fn init_fxaa_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + let texture_bind_group = render_device.create_bind_group_layout( + "fxaa_texture_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), ), - ); + ), + ); - let sampler = render_device.create_sampler(&SamplerDescriptor { - mipmap_filter: FilterMode::Linear, - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - ..default() - }); + let sampler = render_device.create_sampler(&SamplerDescriptor { + mipmap_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); - FxaaPipeline { - texture_bind_group, - sampler, - } - } + commands.insert_resource(FxaaPipeline { + texture_bind_group, + sampler, + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "fxaa.wgsl"), + }); } #[derive(Component)] @@ -181,25 +179,21 @@ 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: FXAA_SHADER_HANDLE, + 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(), ], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_anti_aliasing/src/lib.rs b/crates/bevy_anti_aliasing/src/lib.rs index be09a2e5b2..12b7982cb5 100644 --- a/crates/bevy_anti_aliasing/src/lib.rs +++ b/crates/bevy_anti_aliasing/src/lib.rs @@ -2,26 +2,25 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] use bevy_app::Plugin; use contrast_adaptive_sharpening::CasPlugin; use fxaa::FxaaPlugin; use smaa::SmaaPlugin; +use taa::TemporalAntiAliasPlugin; pub mod contrast_adaptive_sharpening; -pub mod experimental; pub mod fxaa; pub mod smaa; - -mod taa; +pub mod taa; #[derive(Default)] pub struct AntiAliasingPlugin; impl Plugin for AntiAliasingPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_plugins((FxaaPlugin, CasPlugin, SmaaPlugin)); + app.add_plugins((FxaaPlugin, SmaaPlugin, TemporalAntiAliasPlugin, CasPlugin)); } } diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_aliasing/src/smaa/mod.rs index 4259b5e33d..33a916e489 100644 --- a/crates/bevy_anti_aliasing/src/smaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/smaa/mod.rs @@ -30,9 +30,7 @@ //! //! [SMAA]: https://www.iryoku.com/smaa/ use bevy_app::{App, Plugin}; -#[cfg(feature = "smaa_luts")] -use bevy_asset::load_internal_binary_asset; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; #[cfg(not(feature = "smaa_luts"))] use bevy_core_pipeline::tonemapping::lut_placeholder; use bevy_core_pipeline::{ @@ -48,9 +46,9 @@ use bevy_ecs::{ resource::Resource, schedule::IntoScheduleConfigs as _, system::{lifetimeless::Read, Commands, Query, Res, ResMut}, - world::{FromWorld, World}, + world::World, }; -use bevy_image::{BevyDefault, Image}; +use bevy_image::{BevyDefault, Image, ToExtents}; use bevy_math::{vec4, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ @@ -58,37 +56,27 @@ use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, render_asset::RenderAssets, render_graph::{ - NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner, }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, - DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, LoadOp, MultisampleState, - Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, - RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal, - ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, - StencilFaceState, StencilOperation, StencilState, StoreOp, TextureDescriptor, - TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView, - VertexState, + DynamicUniformBuffer, FilterMode, FragmentState, LoadOp, Operations, PipelineCache, + RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, + RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader, + ShaderDefVal, ShaderStages, ShaderType, SpecializedRenderPipeline, + SpecializedRenderPipelines, StencilFaceState, StencilOperation, StencilState, StoreOp, + TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + TextureView, VertexState, }, renderer::{RenderContext, RenderDevice, RenderQueue}, texture::{CachedTexture, GpuImage, TextureCache}, view::{ExtractedView, ViewTarget}, - Render, RenderApp, RenderSystems, + Render, RenderApp, RenderStartup, RenderSystems, }; use bevy_utils::prelude::default; -/// The handle of the `smaa.wgsl` shader. -const SMAA_SHADER_HANDLE: Handle = weak_handle!("fdd9839f-1ab4-4e0d-88a0-240b67da2ddf"); -/// The handle of the area LUT, a KTX2 format texture that SMAA uses internally. -const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle = - weak_handle!("569c4d67-c7fa-4958-b1af-0836023603c0"); -/// The handle of the search LUT, a KTX2 format texture that SMAA uses internally. -const SMAA_SEARCH_LUT_TEXTURE_HANDLE: Handle = - weak_handle!("43b97515-252e-4c8a-b9af-f2fc528a1c27"); - /// Adds support for subpixel morphological antialiasing, or SMAA. pub struct SmaaPlugin; @@ -128,6 +116,14 @@ pub enum SmaaPreset { Ultra, } +#[derive(Resource)] +struct SmaaLuts { + /// The handle of the area LUT, a KTX2 format texture that SMAA uses internally. + area_lut: Handle, + /// The handle of the search LUT, a KTX2 format texture that SMAA uses internally. + search_lut: Handle, +} + /// A render world resource that holds all render pipeline data needed for SMAA. /// /// There are three separate passes, so we need three separate pipelines. @@ -147,6 +143,8 @@ struct SmaaEdgeDetectionPipeline { postprocess_bind_group_layout: BindGroupLayout, /// The bind group layout for data specific to this pass. edge_detection_bind_group_layout: BindGroupLayout, + /// The shader asset handle. + shader: Handle, } /// The pipeline data for phase 2 of SMAA: blending weight calculation. @@ -155,6 +153,8 @@ struct SmaaBlendingWeightCalculationPipeline { postprocess_bind_group_layout: BindGroupLayout, /// The bind group layout for data specific to this pass. blending_weight_calculation_bind_group_layout: BindGroupLayout, + /// The shader asset handle. + shader: Handle, } /// The pipeline data for phase 3 of SMAA: neighborhood blending. @@ -163,6 +163,8 @@ struct SmaaNeighborhoodBlendingPipeline { postprocess_bind_group_layout: BindGroupLayout, /// The bind group layout for data specific to this pass. neighborhood_blending_bind_group_layout: BindGroupLayout, + /// The shader asset handle. + shader: Handle, } /// A unique identifier for a set of SMAA pipelines. @@ -287,51 +289,28 @@ pub struct SmaaSpecializedRenderPipelines { impl Plugin for SmaaPlugin { fn build(&self, app: &mut App) { // Load the shader. - load_internal_asset!(app, SMAA_SHADER_HANDLE, "smaa.wgsl", Shader::from_wgsl); - - // Load the two lookup textures. These are compressed textures in KTX2 - // format. - #[cfg(feature = "smaa_luts")] - load_internal_binary_asset!( - app, - SMAA_AREA_LUT_TEXTURE_HANDLE, - "SMAAAreaLUT.ktx2", - |bytes, _: String| Image::from_buffer( - bytes, - bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2), - bevy_image::CompressedImageFormats::NONE, - false, - bevy_image::ImageSampler::Default, - bevy_asset::RenderAssetUsages::RENDER_WORLD, - ) - .expect("Failed to load SMAA area LUT") - ); + embedded_asset!(app, "smaa.wgsl"); #[cfg(feature = "smaa_luts")] - load_internal_binary_asset!( - app, - SMAA_SEARCH_LUT_TEXTURE_HANDLE, - "SMAASearchLUT.ktx2", - |bytes, _: String| Image::from_buffer( - bytes, - bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2), - bevy_image::CompressedImageFormats::NONE, - false, - bevy_image::ImageSampler::Default, - bevy_asset::RenderAssetUsages::RENDER_WORLD, - ) - .expect("Failed to load SMAA search LUT") - ); + let smaa_luts = { + // Load the two lookup textures. These are compressed textures in KTX2 format. + embedded_asset!(app, "SMAAAreaLUT.ktx2"); + embedded_asset!(app, "SMAASearchLUT.ktx2"); + SmaaLuts { + area_lut: load_embedded_asset!(app, "SMAAAreaLUT.ktx2"), + search_lut: load_embedded_asset!(app, "SMAASearchLUT.ktx2"), + } + }; #[cfg(not(feature = "smaa_luts"))] - app.world_mut() - .resource_mut::>() - .insert(SMAA_AREA_LUT_TEXTURE_HANDLE.id(), lut_placeholder()); - - #[cfg(not(feature = "smaa_luts"))] - app.world_mut() - .resource_mut::>() - .insert(SMAA_SEARCH_LUT_TEXTURE_HANDLE.id(), lut_placeholder()); + let smaa_luts = { + let mut images = app.world_mut().resource_mut::>(); + let handle = images.add(lut_placeholder()); + SmaaLuts { + area_lut: handle.clone(), + search_lut: handle.clone(), + } + }; app.add_plugins(ExtractComponentPlugin::::default()) .register_type::(); @@ -341,8 +320,10 @@ impl Plugin for SmaaPlugin { }; render_app + .insert_resource(smaa_luts) .init_resource::() .init_resource::() + .add_systems(RenderStartup, init_smaa_pipelines) .add_systems( Render, ( @@ -371,81 +352,79 @@ impl Plugin for SmaaPlugin { ), ); } - - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::(); - } - } } -impl FromWorld for SmaaPipelines { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - - // Create the postprocess bind group layout (all passes, bind group 0). - let postprocess_bind_group_layout = render_device.create_bind_group_layout( - "SMAA postprocess bind group layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: true }), - uniform_buffer::(true) - .visibility(ShaderStages::VERTEX_FRAGMENT), - ), +pub fn init_smaa_pipelines( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + // Create the postprocess bind group layout (all passes, bind group 0). + let postprocess_bind_group_layout = render_device.create_bind_group_layout( + "SMAA postprocess bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + uniform_buffer::(true).visibility(ShaderStages::VERTEX_FRAGMENT), ), - ); + ), + ); - // Create the edge detection bind group layout (pass 1, bind group 1). - let edge_detection_bind_group_layout = render_device.create_bind_group_layout( - "SMAA edge detection bind group layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - (sampler(SamplerBindingType::Filtering),), + // Create the edge detection bind group layout (pass 1, bind group 1). + let edge_detection_bind_group_layout = render_device.create_bind_group_layout( + "SMAA edge detection bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + (sampler(SamplerBindingType::Filtering),), + ), + ); + + // Create the blending weight calculation bind group layout (pass 2, bind group 1). + let blending_weight_calculation_bind_group_layout = render_device.create_bind_group_layout( + "SMAA blending weight calculation bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), // edges texture + sampler(SamplerBindingType::Filtering), // edges sampler + texture_2d(TextureSampleType::Float { filterable: true }), // search texture + texture_2d(TextureSampleType::Float { filterable: true }), // area texture ), - ); + ), + ); - // Create the blending weight calculation bind group layout (pass 2, bind group 1). - let blending_weight_calculation_bind_group_layout = render_device.create_bind_group_layout( - "SMAA blending weight calculation bind group layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: true }), // edges texture - sampler(SamplerBindingType::Filtering), // edges sampler - texture_2d(TextureSampleType::Float { filterable: true }), // search texture - texture_2d(TextureSampleType::Float { filterable: true }), // area texture - ), + // Create the neighborhood blending bind group layout (pass 3, bind group 1). + let neighborhood_blending_bind_group_layout = render_device.create_bind_group_layout( + "SMAA neighborhood blending bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), ), - ); + ), + ); - // Create the neighborhood blending bind group layout (pass 3, bind group 1). - let neighborhood_blending_bind_group_layout = render_device.create_bind_group_layout( - "SMAA neighborhood blending bind group layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: true }), - sampler(SamplerBindingType::Filtering), - ), - ), - ); + let shader = load_embedded_asset!(asset_server.as_ref(), "smaa.wgsl"); - SmaaPipelines { - edge_detection: SmaaEdgeDetectionPipeline { - postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), - edge_detection_bind_group_layout, - }, - blending_weight_calculation: SmaaBlendingWeightCalculationPipeline { - postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), - blending_weight_calculation_bind_group_layout, - }, - neighborhood_blending: SmaaNeighborhoodBlendingPipeline { - postprocess_bind_group_layout, - neighborhood_blending_bind_group_layout, - }, - } - } + commands.insert_resource(SmaaPipelines { + edge_detection: SmaaEdgeDetectionPipeline { + postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), + edge_detection_bind_group_layout, + shader: shader.clone(), + }, + blending_weight_calculation: SmaaBlendingWeightCalculationPipeline { + postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), + blending_weight_calculation_bind_group_layout, + shader: shader.clone(), + }, + neighborhood_blending: SmaaNeighborhoodBlendingPipeline { + postprocess_bind_group_layout, + neighborhood_blending_bind_group_layout, + shader, + }, + }); } // Phase 1: edge detection. @@ -472,23 +451,21 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline { self.edge_detection_bind_group_layout.clone(), ], vertex: VertexState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "edge_detection_vertex_main".into(), + entry_point: Some("edge_detection_vertex_main".into()), buffers: vec![], }, fragment: Some(FragmentState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "luma_edge_detection_fragment_main".into(), + entry_point: Some("luma_edge_detection_fragment_main".into()), targets: vec![Some(ColorTargetState { format: TextureFormat::Rg8Unorm, blend: None, write_mask: ColorWrites::ALL, })], }), - push_constant_ranges: vec![], - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: TextureFormat::Stencil8, depth_write_enabled: false, @@ -501,8 +478,7 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline { }, bias: default(), }), - multisample: MultisampleState::default(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -532,23 +508,21 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline { self.blending_weight_calculation_bind_group_layout.clone(), ], vertex: VertexState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "blending_weight_calculation_vertex_main".into(), + entry_point: Some("blending_weight_calculation_vertex_main".into()), buffers: vec![], }, fragment: Some(FragmentState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "blending_weight_calculation_fragment_main".into(), + entry_point: Some("blending_weight_calculation_fragment_main".into()), targets: vec![Some(ColorTargetState { format: TextureFormat::Rgba8Unorm, blend: None, write_mask: ColorWrites::ALL, })], }), - push_constant_ranges: vec![], - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: TextureFormat::Stencil8, depth_write_enabled: false, @@ -561,8 +535,7 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline { }, bias: default(), }), - multisample: MultisampleState::default(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -580,26 +553,22 @@ impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline { self.neighborhood_blending_bind_group_layout.clone(), ], vertex: VertexState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "neighborhood_blending_vertex_main".into(), + entry_point: Some("neighborhood_blending_vertex_main".into()), buffers: vec![], }, fragment: Some(FragmentState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "neighborhood_blending_fragment_main".into(), + entry_point: Some("neighborhood_blending_fragment_main".into()), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], }), - push_constant_ranges: vec![], - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -695,18 +664,12 @@ fn prepare_smaa_textures( continue; }; - let texture_size = Extent3d { - width: texture_size.x, - height: texture_size.y, - depth_or_array_layers: 1, - }; - // Create the two-channel RG texture for phase 1 (edge detection). let edge_detection_color_texture = texture_cache.get( &render_device, TextureDescriptor { label: Some("SMAA edge detection color texture"), - size: texture_size, + size: texture_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -721,7 +684,7 @@ fn prepare_smaa_textures( &render_device, TextureDescriptor { label: Some("SMAA edge detection stencil texture"), - size: texture_size, + size: texture_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -737,7 +700,7 @@ fn prepare_smaa_textures( &render_device, TextureDescriptor { label: Some("SMAA blend texture"), - size: texture_size, + size: texture_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -761,13 +724,14 @@ fn prepare_smaa_bind_groups( mut commands: Commands, render_device: Res, smaa_pipelines: Res, + smaa_luts: Res, images: Res>, view_targets: Query<(Entity, &SmaaTextures), (With, With)>, ) { // Fetch the two lookup textures. These are bundled in this library. let (Some(search_texture), Some(area_texture)) = ( - images.get(&SMAA_SEARCH_LUT_TEXTURE_HANDLE), - images.get(&SMAA_AREA_LUT_TEXTURE_HANDLE), + images.get(&smaa_luts.search_lut), + images.get(&smaa_luts.area_lut), ) else { return; }; @@ -838,7 +802,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/smaa/smaa.wgsl b/crates/bevy_anti_aliasing/src/smaa/smaa.wgsl index 0872325448..24dc6baa25 100644 --- a/crates/bevy_anti_aliasing/src/smaa/smaa.wgsl +++ b/crates/bevy_anti_aliasing/src/smaa/smaa.wgsl @@ -146,10 +146,10 @@ * * (See SMAA_INCLUDE_VS and SMAA_INCLUDE_PS below). * * And four presets: - * SMAA_PRESET_LOW (%60 of the quality) - * SMAA_PRESET_MEDIUM (%80 of the quality) - * SMAA_PRESET_HIGH (%95 of the quality) - * SMAA_PRESET_ULTRA (%99 of the quality) + * SMAA_PRESET_LOW (60% of the quality) + * SMAA_PRESET_MEDIUM (80% of the quality) + * SMAA_PRESET_HIGH (95% of the quality) + * SMAA_PRESET_ULTRA (99% of the quality) * * For example: * #define SMAA_RT_METRICS float4(1.0 / 1280.0, 1.0 / 720.0, 1280.0, 720.0) diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_aliasing/src/taa/mod.rs index dc12d34423..f182108f10 100644 --- a/crates/bevy_anti_aliasing/src/taa/mod.rs +++ b/crates/bevy_anti_aliasing/src/taa/mod.rs @@ -1,10 +1,10 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, 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::{ @@ -13,35 +13,34 @@ use bevy_ecs::{ resource::Resource, schedule::IntoScheduleConfigs, system::{Commands, Query, Res, ResMut}, - world::{FromWorld, World}, + world::World, }; -use bevy_image::BevyDefault as _; +use bevy_image::{BevyDefault as _, ToExtents}; use bevy_math::vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::{ExtractedCamera, MipBias, TemporalJitter}, prelude::{Camera, Projection}, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{ binding_types::{sampler, texture_2d, texture_depth_2d}, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, - ColorTargetState, ColorWrites, Extent3d, FilterMode, FragmentState, MultisampleState, - Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, - RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, - ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor, - TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + ColorTargetState, ColorWrites, FilterMode, FragmentState, Operations, PipelineCache, + RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler, + SamplerBindingType, SamplerDescriptor, Shader, ShaderStages, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureDescriptor, TextureDimension, TextureFormat, + TextureSampleType, TextureUsages, }, renderer::{RenderContext, RenderDevice}, sync_component::SyncComponentPlugin, sync_world::RenderEntity, texture::{CachedTexture, TextureCache}, view::{ExtractedView, Msaa, ViewTarget}, - ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems, + ExtractSchedule, MainWorld, Render, RenderApp, RenderStartup, RenderSystems, }; +use bevy_utils::default; use tracing::warn; -const TAA_SHADER_HANDLE: Handle = weak_handle!("fea20d50-86b6-4069-aa32-374346aec00c"); - /// Plugin for temporal anti-aliasing. /// /// See [`TemporalAntiAliasing`] for more details. @@ -49,7 +48,7 @@ pub struct TemporalAntiAliasPlugin; impl Plugin for TemporalAntiAliasPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, TAA_SHADER_HANDLE, "taa.wgsl", Shader::from_wgsl); + embedded_asset!(app, "taa.wgsl"); app.register_type::(); @@ -60,11 +59,12 @@ impl Plugin for TemporalAntiAliasPlugin { }; render_app .init_resource::>() + .add_systems(RenderStartup, init_taa_pipeline) .add_systems(ExtractSchedule, extract_taa_settings) .add_systems( Render, ( - prepare_taa_jitter_and_mip_bias.in_set(RenderSystems::ManageViews), + prepare_taa_jitter.in_set(RenderSystems::ManageViews), prepare_taa_pipelines.in_set(RenderSystems::Prepare), prepare_taa_history_textures.in_set(RenderSystems::PrepareResources), ), @@ -81,14 +81,6 @@ impl Plugin for TemporalAntiAliasPlugin { ), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app.init_resource::(); - } } /// Component to apply temporal anti-aliasing to a 3D perspective camera. @@ -115,7 +107,6 @@ impl Plugin for TemporalAntiAliasPlugin { /// /// # Usage Notes /// -/// The [`TemporalAntiAliasPlugin`] must be added to your app. /// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`]. /// /// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`]. @@ -128,11 +119,9 @@ impl Plugin for TemporalAntiAliasPlugin { /// /// 1. Write particle motion vectors to the motion vectors prepass texture /// 2. Render particles after TAA -/// -/// If no [`MipBias`] component is attached to the camera, TAA will add a `MipBias(-1.0)` component. #[derive(Component, Reflect, Clone)] #[reflect(Component, Default, Clone)] -#[require(TemporalJitter, DepthPrepass, MotionVectorPrepass)] +#[require(TemporalJitter, MipBias, DepthPrepass, MotionVectorPrepass)] #[doc(alias = "Taa")] pub struct TemporalAntiAliasing { /// Set to true to delete the saved temporal history (past frames). @@ -243,52 +232,57 @@ struct TaaPipeline { taa_bind_group_layout: BindGroupLayout, nearest_sampler: Sampler, linear_sampler: Sampler, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } -impl FromWorld for TaaPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); +fn init_taa_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + let nearest_sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("taa_nearest_sampler"), + mag_filter: FilterMode::Nearest, + min_filter: FilterMode::Nearest, + ..SamplerDescriptor::default() + }); + let linear_sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("taa_linear_sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..SamplerDescriptor::default() + }); - let nearest_sampler = render_device.create_sampler(&SamplerDescriptor { - label: Some("taa_nearest_sampler"), - mag_filter: FilterMode::Nearest, - min_filter: FilterMode::Nearest, - ..SamplerDescriptor::default() - }); - let linear_sampler = render_device.create_sampler(&SamplerDescriptor { - label: Some("taa_linear_sampler"), - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - ..SamplerDescriptor::default() - }); - - let taa_bind_group_layout = render_device.create_bind_group_layout( - "taa_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - // View target (read) - texture_2d(TextureSampleType::Float { filterable: true }), - // TAA History (read) - texture_2d(TextureSampleType::Float { filterable: true }), - // Motion Vectors - texture_2d(TextureSampleType::Float { filterable: true }), - // Depth - texture_depth_2d(), - // Nearest sampler - sampler(SamplerBindingType::NonFiltering), - // Linear sampler - sampler(SamplerBindingType::Filtering), - ), + let taa_bind_group_layout = render_device.create_bind_group_layout( + "taa_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // View target (read) + texture_2d(TextureSampleType::Float { filterable: true }), + // TAA History (read) + texture_2d(TextureSampleType::Float { filterable: true }), + // Motion Vectors + texture_2d(TextureSampleType::Float { filterable: true }), + // Depth + texture_depth_2d(), + // Nearest sampler + sampler(SamplerBindingType::NonFiltering), + // Linear sampler + sampler(SamplerBindingType::Filtering), ), - ); + ), + ); - TaaPipeline { - taa_bind_group_layout, - nearest_sampler, - linear_sampler, - } - } + commands.insert_resource(TaaPipeline { + taa_bind_group_layout, + nearest_sampler, + linear_sampler, + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "taa.wgsl"), + }); } #[derive(PartialEq, Eq, Hash, Clone)] @@ -317,11 +311,10 @@ 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: TAA_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point: "taa".into(), targets: vec![ Some(ColorTargetState { format, @@ -334,27 +327,19 @@ impl SpecializedRenderPipeline for TaaPipeline { write_mask: ColorWrites::ALL, }), ], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut) { - let mut cameras_3d = main_world.query_filtered::<( + let mut cameras_3d = main_world.query::<( RenderEntity, &Camera, &Projection, - &mut TemporalAntiAliasing, - ), ( - With, - With, - With, - With, + Option<&mut TemporalAntiAliasing>, )>(); for (entity, camera, camera_projection, mut taa_settings) in @@ -364,14 +349,12 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut(); @@ -379,13 +362,22 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut, - mut query: Query<(Entity, &mut TemporalJitter, Option<&MipBias>), With>, - mut commands: Commands, + mut query: Query< + &mut TemporalJitter, + ( + With, + With, + With, + With, + With, + ), + >, ) { - // Halton sequence (2, 3) - 0.5, skipping i = 0 + // Halton sequence (2, 3) - 0.5 let halton_sequence = [ + vec2(0.0, 0.0), vec2(0.0, -0.16666666), vec2(-0.25, 0.16666669), vec2(0.25, -0.3888889), @@ -393,17 +385,12 @@ fn prepare_taa_jitter_and_mip_bias( vec2(0.125, 0.2777778), vec2(-0.125, -0.2777778), vec2(0.375, 0.055555582), - vec2(-0.4375, 0.3888889), ]; let offset = halton_sequence[frame_count.0 as usize % halton_sequence.len()]; - for (entity, mut jitter, mip_bias) in &mut query { + for mut jitter in &mut query { jitter.offset = offset; - - if mip_bias.is_none() { - commands.entity(entity).insert(MipBias(-1.0)); - } } } @@ -424,11 +411,7 @@ fn prepare_taa_history_textures( if let Some(physical_target_size) = camera.physical_target_size { let mut texture_descriptor = TextureDescriptor { label: None, - size: Extent3d { - depth_or_array_layers: 1, - width: physical_target_size.x, - height: physical_target_size.y, - }, + size: physical_target_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index f46db94db3..a0c5222b0b 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -47,7 +47,6 @@ std = [ "bevy_ecs/std", "dep:ctrlc", "downcast-rs/std", - "bevy_utils/std", "bevy_tasks/std", "bevy_platform/std", ] @@ -72,16 +71,20 @@ web = [ "dep:console_error_panic_hook", ] +hotpatching = [ + "bevy_ecs/hotpatching", + "dep:dioxus-devtools", + "dep:crossbeam-channel", +] + [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, features = [ - "alloc", -] } -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 } @@ -90,8 +93,10 @@ variadics_please = "1.1" tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } cfg-if = "1.0.0" +dioxus-devtools = { version = "0.7.0-alpha.1", optional = true } +crossbeam-channel = { version = "0.5.0", optional = true } -[target.'cfg(any(unix, windows))'.dependencies] +[target.'cfg(any(all(unix, not(target_os = "horizon")), windows))'.dependencies] ctrlc = { version = "3.4.4", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 60431ca479..a9057c787d 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -10,6 +10,7 @@ use alloc::{ pub use bevy_derive::AppLabel; use bevy_ecs::{ component::RequiredComponentsError, + error::{DefaultErrorHandler, ErrorHandler}, event::{event_update_system, EventCursor}, intern::Interned, prelude::*, @@ -85,6 +86,7 @@ pub struct App { /// [`WinitPlugin`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html /// [`ScheduleRunnerPlugin`]: https://docs.rs/bevy/latest/bevy/app/struct.ScheduleRunnerPlugin.html pub(crate) runner: RunnerFn, + default_error_handler: Option, } impl Debug for App { @@ -104,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")] @@ -143,6 +148,7 @@ impl App { sub_apps: HashMap::default(), }, runner: Box::new(run_once), + default_error_handler: None, } } @@ -338,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. @@ -349,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(); /// # @@ -357,7 +363,7 @@ impl App { /// ``` pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { self.main_mut().add_event::(); self @@ -1115,7 +1121,12 @@ impl App { } /// Inserts a [`SubApp`] with the given label. - pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) { + pub fn insert_sub_app(&mut self, label: impl AppLabel, mut sub_app: SubApp) { + if let Some(handler) = self.default_error_handler { + sub_app + .world_mut() + .get_resource_or_insert_with(|| DefaultErrorHandler(handler)); + } self.sub_apps.sub_apps.insert(label.intern(), sub_app); } @@ -1298,6 +1309,8 @@ 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 [`On`]. + /// /// # Examples /// /// ```rust @@ -1312,14 +1325,14 @@ impl App { /// # friends_allowed: bool, /// # }; /// # - /// # #[derive(Event)] + /// # #[derive(Event, EntityEvent)] /// # struct Invite; /// # /// # #[derive(Component)] /// # struct Friend; /// # - /// // An observer system can be any system where the first parameter is a trigger - /// 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); @@ -1334,6 +1347,49 @@ impl App { self.world_mut().add_observer(observer); self } + + /// Gets the error handler to set for new supapps. + /// + /// Note that the error handler of existing subapps may differ. + pub fn get_error_handler(&self) -> Option { + self.default_error_handler + } + + /// Set the [default error handler] for the all subapps (including the main one and future ones) + /// that do not have one. + /// + /// May only be called once and should be set by the application, not by libraries. + /// + /// The handler will be called when an error is produced and not otherwise handled. + /// + /// # Panics + /// Panics if called multiple times. + /// + /// # Example + /// ``` + /// # use bevy_app::*; + /// # use bevy_ecs::error::warn; + /// # fn MyPlugins(_: &mut App) {} + /// App::new() + /// .set_error_handler(warn) + /// .add_plugins(MyPlugins) + /// .run(); + /// ``` + /// + /// [default error handler]: bevy_ecs::error::DefaultErrorHandler + pub fn set_error_handler(&mut self, handler: ErrorHandler) -> &mut Self { + assert!( + self.default_error_handler.is_none(), + "`set_error_handler` called multiple times on same `App`" + ); + self.default_error_handler = Some(handler); + for sub_app in self.sub_apps.iter_mut() { + sub_app + .world_mut() + .get_resource_or_insert_with(|| DefaultErrorHandler(handler)); + } + self + } } type RunnerFn = Box AppExit>; @@ -1351,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 @@ -1361,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] @@ -1429,9 +1485,9 @@ mod tests { change_detection::{DetectChanges, ResMut}, component::Component, entity::Entity, - event::{Event, EventWriter, Events}, + event::{BufferedEvent, Event, EventWriter, Events}, + lifecycle::RemovedComponents, query::With, - removal_detection::RemovedComponents, resource::Resource, schedule::{IntoScheduleConfigs, ScheduleLabel}, system::{Commands, Query}, @@ -1526,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] @@ -1795,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(); @@ -1808,7 +1864,7 @@ mod tests { app.update(); // Sending one event - app.world_mut().send_event(TestEvent); + app.world_mut().write_event(TestEvent); let test_events = app.world().resource::>(); assert_eq!(test_events.len(), 1); @@ -1816,8 +1872,8 @@ mod tests { app.update(); // Sending two events on the next frame - app.world_mut().send_event(TestEvent); - app.world_mut().send_event(TestEvent); + app.world_mut().write_event(TestEvent); + app.world_mut().write_event(TestEvent); let test_events = app.world().resource::>(); assert_eq!(test_events.len(), 3); // Events are double-buffered, so we see 1 + 2 = 3 diff --git a/crates/bevy_app/src/hotpatch.rs b/crates/bevy_app/src/hotpatch.rs new file mode 100644 index 0000000000..1f9da40730 --- /dev/null +++ b/crates/bevy_app/src/hotpatch.rs @@ -0,0 +1,42 @@ +//! Utilities for hotpatching code. +extern crate alloc; + +use alloc::sync::Arc; + +use bevy_ecs::{event::EventWriter, HotPatched}; +#[cfg(not(target_family = "wasm"))] +use dioxus_devtools::connect_subsecond; +use dioxus_devtools::subsecond; + +pub use dioxus_devtools::subsecond::{call, HotFunction}; + +use crate::{Last, Plugin}; + +/// Plugin connecting to Dioxus CLI to enable hot patching. +#[derive(Default)] +pub struct HotPatchPlugin; + +impl Plugin for HotPatchPlugin { + fn build(&self, app: &mut crate::App) { + let (sender, receiver) = crossbeam_channel::bounded::(1); + + // Connects to the dioxus CLI that will handle rebuilds + // This will open a connection to the dioxus CLI to receive updated jump tables + // Sends a `HotPatched` message through the channel when the jump table is updated + #[cfg(not(target_family = "wasm"))] + connect_subsecond(); + subsecond::register_handler(Arc::new(move || { + sender.send(HotPatched).unwrap(); + })); + + // Adds a system that will read the channel for new `HotPatched`, and forward them as event to the ECS + app.add_event::().add_systems( + Last, + move |mut events: EventWriter| { + if receiver.try_recv().is_ok() { + events.write_default(); + } + }, + ); + } +} diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 743806df71..188ba957f6 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -8,8 +8,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -28,21 +28,26 @@ mod main_schedule; mod panic_handler; mod plugin; mod plugin_group; +mod propagate; mod schedule_runner; mod sub_app; mod task_pool_plugin; -#[cfg(all(any(unix, windows), feature = "std"))] +#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))] mod terminal_ctrl_c_handler; +#[cfg(feature = "hotpatching")] +pub mod hotpatch; + pub use app::*; pub use main_schedule::*; pub use panic_handler::*; pub use plugin::*; pub use plugin_group::*; +pub use propagate::*; pub use schedule_runner::*; pub use sub_app::*; pub use task_pool_plugin::*; -#[cfg(all(any(unix, windows), feature = "std"))] +#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))] pub use terminal_ctrl_c_handler::*; /// The app prelude. diff --git a/crates/bevy_app/src/panic_handler.rs b/crates/bevy_app/src/panic_handler.rs index 1021a3dc2e..c35d2333bf 100644 --- a/crates/bevy_app/src/panic_handler.rs +++ b/crates/bevy_app/src/panic_handler.rs @@ -1,4 +1,4 @@ -//! This module provides panic handlers for [Bevy](https://bevyengine.org) +//! This module provides panic handlers for [Bevy](https://bevy.org) //! apps, and automatically configures platform specifics (i.e. Wasm or Android). //! //! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`. diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs new file mode 100644 index 0000000000..c6ac5139b9 --- /dev/null +++ b/crates/bevy_app/src/propagate.rs @@ -0,0 +1,552 @@ +use alloc::vec::Vec; +use core::marker::PhantomData; + +use crate::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + hierarchy::ChildOf, + lifecycle::RemovedComponents, + query::{Changed, Or, QueryFilter, With, Without}, + relationship::{Relationship, RelationshipTarget}, + schedule::{IntoScheduleConfigs, SystemSet}, + system::{Commands, Local, Query}, +}; + +/// Plugin to automatically propagate a component value to all direct and transient relationship +/// targets (e.g. [`bevy_ecs::hierarchy::Children`]) of entities with a [`Propagate`] component. +/// +/// The plugin Will maintain the target component over hierarchy changes, adding or removing +/// `C` when a relationship `R` (e.g. [`ChildOf`]) is added to or removed from a +/// relationship tree with a [`Propagate`] source, or if the [`Propagate`] component +/// is added, changed or removed. +/// +/// Optionally you can include a query filter `F` to restrict the entities that are updated. +/// Note that the filter is not rechecked dynamically: changes to the filter state will not be +/// picked up until the [`Propagate`] component is touched, or the hierarchy is changed. +/// All members of the tree between source and target must match the filter for propagation +/// to reach a given target. +/// Individual entities can be skipped or terminate the propagation with the [`PropagateOver`] +/// and [`PropagateStop`] components. +pub struct HierarchyPropagatePlugin< + C: Component + Clone + PartialEq, + F: QueryFilter = (), + R: Relationship = ChildOf, +>(PhantomData (C, F, R)>); + +/// Causes the inner component to be added to this entity and all direct and transient relationship +/// targets. A target with a [`Propagate`] component of its own will override propagation from +/// that point in the tree. +#[derive(Component, Clone, PartialEq)] +pub struct Propagate(pub C); + +/// Stops the output component being added to this entity. +/// Relationship targets will still inherit the component from this entity or its parents. +#[derive(Component)] +pub struct PropagateOver(PhantomData C>); + +/// Stops the propagation at this entity. Children will not inherit the component. +#[derive(Component)] +pub struct PropagateStop(PhantomData C>); + +/// The set in which propagation systems are added. You can schedule your logic relative to this set. +#[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)] +pub struct PropagateSet { + _p: PhantomData C>, +} + +/// Internal struct for managing propagation +#[derive(Component, Clone, PartialEq)] +pub struct Inherited(pub C); + +impl Default + for HierarchyPropagatePlugin +{ + fn default() -> Self { + Self(Default::default()) + } +} + +impl Default for PropagateOver { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Default for PropagateStop { + fn default() -> Self { + Self(Default::default()) + } +} + +impl core::fmt::Debug for PropagateSet { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PropagateSet") + .field("_p", &self._p) + .finish() + } +} + +impl Eq for PropagateSet {} + +impl core::hash::Hash for PropagateSet { + fn hash(&self, state: &mut H) { + self._p.hash(state); + } +} + +impl Default for PropagateSet { + fn default() -> Self { + Self { + _p: Default::default(), + } + } +} + +impl Plugin + for HierarchyPropagatePlugin +{ + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + update_source::, + update_stopped::, + update_reparented::, + propagate_inherited::, + propagate_output::, + ) + .chain() + .in_set(PropagateSet::::default()), + ); + } +} + +/// add/remove `Inherited::` and `C` for entities with a direct `Propagate::` +pub fn update_source( + mut commands: Commands, + changed: Query< + (Entity, &Propagate), + ( + Or<(Changed>, Without>)>, + Without>, + ), + >, + mut removed: RemovedComponents>, +) { + for (entity, source) in &changed { + commands + .entity(entity) + .try_insert(Inherited(source.0.clone())); + } + + for removed in removed.read() { + if let Ok(mut commands) = commands.get_entity(removed) { + commands.remove::<(Inherited, C)>(); + } + } +} + +/// remove `Inherited::` and `C` for entities with a `PropagateStop::` +pub fn update_stopped( + mut commands: Commands, + q: Query>, With>, F)>, +) { + for entity in q.iter() { + let mut cmds = commands.entity(entity); + cmds.remove::<(Inherited, C)>(); + } +} + +/// add/remove `Inherited::` and `C` for entities which have changed relationship +pub fn update_reparented( + mut commands: Commands, + moved: Query< + (Entity, &R, Option<&Inherited>), + ( + Changed, + Without>, + Without>, + F, + ), + >, + relations: Query<&Inherited>, + orphaned: Query>, Without>, Without, F)>, +) { + for (entity, relation, maybe_inherited) in &moved { + if let Ok(inherited) = relations.get(relation.get()) { + commands.entity(entity).try_insert(inherited.clone()); + } else if maybe_inherited.is_some() { + commands.entity(entity).remove::<(Inherited, C)>(); + } + } + + for orphan in &orphaned { + commands.entity(orphan).remove::<(Inherited, C)>(); + } +} + +/// add/remove `Inherited::` for targets of entities with modified `Inherited::` +pub fn propagate_inherited( + mut commands: Commands, + changed: Query< + (&Inherited, &R::RelationshipTarget), + (Changed>, Without>, F), + >, + recurse: Query< + (Option<&R::RelationshipTarget>, Option<&Inherited>), + (Without>, Without>, F), + >, + mut removed: RemovedComponents>, + mut to_process: Local>)>>, +) { + // gather changed + for (inherited, targets) in &changed { + to_process.extend( + targets + .iter() + .map(|target| (target, Some(inherited.clone()))), + ); + } + + // and removed + for entity in removed.read() { + if let Ok((Some(targets), _)) = recurse.get(entity) { + to_process.extend(targets.iter().map(|target| (target, None))); + } + } + + // propagate + while let Some((entity, maybe_inherited)) = (*to_process).pop() { + let Ok((maybe_targets, maybe_current)) = recurse.get(entity) else { + continue; + }; + + if maybe_current == maybe_inherited.as_ref() { + continue; + } + + if let Some(targets) = maybe_targets { + to_process.extend( + targets + .iter() + .map(|target| (target, maybe_inherited.clone())), + ); + } + + if let Some(inherited) = maybe_inherited { + commands.entity(entity).try_insert(inherited.clone()); + } else { + commands.entity(entity).remove::<(Inherited, C)>(); + } + } +} + +/// add `C` to entities with `Inherited::` +pub fn propagate_output( + mut commands: Commands, + changed: Query< + (Entity, &Inherited, Option<&C>), + (Changed>, Without>, F), + >, +) { + for (entity, inherited, maybe_current) in &changed { + if maybe_current.is_some_and(|c| &inherited.0 == c) { + continue; + } + + commands.entity(entity).try_insert(inherited.0.clone()); + } +} + +#[cfg(test)] +mod tests { + use bevy_ecs::schedule::Schedule; + + use crate::{App, Update}; + + use super::*; + + #[derive(Component, Clone, PartialEq, Debug)] + struct TestValue(u32); + + #[test] + fn test_simple_propagate() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let intermediate = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(intermediate)) + .id(); + + app.update(); + + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + } + + #[test] + fn test_reparented() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + } + + #[test] + fn test_reparented_with_prior() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator_a = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagator_b = app.world_mut().spawn(Propagate(TestValue(2))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator_a)) + .id(); + + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(1)) + ); + app.world_mut() + .commands() + .entity(propagatee) + .insert(ChildOf(propagator_b)); + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(2)) + ); + } + + #[test] + fn test_remove_orphan() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + app.world_mut() + .commands() + .entity(propagatee) + .remove::(); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + } + + #[test] + fn test_remove_propagated() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + app.world_mut() + .commands() + .entity(propagator) + .remove::>(); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + } + + #[test] + fn test_propagate_over() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagate_over = app + .world_mut() + .spawn(TestValue(2)) + .insert(ChildOf(propagator)) + .id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagate_over)) + .id(); + + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(1)) + ); + } + + #[test] + fn test_propagate_stop() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagate_stop = app + .world_mut() + .spawn(PropagateStop::::default()) + .insert(ChildOf(propagator)) + .id(); + let no_propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagate_stop)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), no_propagatee) + .is_err()); + } + + #[test] + fn test_intermediate_override() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let intermediate = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(intermediate)) + .id(); + + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(1)) + ); + + app.world_mut() + .entity_mut(intermediate) + .insert(Propagate(TestValue(2))); + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(2)) + ); + } + + #[test] + fn test_filter() { + #[derive(Component)] + struct Marker; + + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::>::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + + // NOTE: changes to the filter condition are not rechecked + app.world_mut().entity_mut(propagator).insert(Marker); + app.world_mut().entity_mut(propagatee).insert(Marker); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + + app.world_mut() + .entity_mut(propagator) + .insert(Propagate(TestValue(1))); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + } +} diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index c340b80654..56a496f2b5 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -12,7 +12,7 @@ use core::fmt::Debug; #[cfg(feature = "trace")] use tracing::info_span; -type ExtractFn = Box; +type ExtractFn = Box; /// A secondary application with its own [`World`]. These can run independently of each other. /// @@ -160,7 +160,7 @@ impl SubApp { /// The first argument is the `World` to extract data from, the second argument is the app `World`. pub fn set_extract(&mut self, extract: F) -> &mut Self where - F: Fn(&mut World, &mut World) + Send + 'static, + F: FnMut(&mut World, &mut World) + Send + 'static, { self.extract = Some(Box::new(extract)); self @@ -177,13 +177,13 @@ impl SubApp { /// ``` /// # use bevy_app::SubApp; /// # let mut app = SubApp::new(); - /// let default_fn = app.take_extract(); + /// let mut default_fn = app.take_extract(); /// app.set_extract(move |main, render| { /// // Do pre-extract custom logic /// // [...] /// /// // Call Bevy's default, which executes the Extract phase - /// if let Some(f) = default_fn.as_ref() { + /// if let Some(f) = default_fn.as_mut() { /// f(main, render); /// } /// @@ -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 07a45a3f6d..edf8986130 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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 43562ae806..0b525b3c1d 100644 --- a/crates/bevy_asset/macros/Cargo.toml +++ b/crates/bevy_asset/macros/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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 10f298c968..b43a8625e7 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -106,7 +106,7 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { /// - Removed assets are not detected. /// /// The list of changed assets only gets updated in the [`AssetEventSystems`] system set, -/// which runs in `Last`. Therefore, `AssetChanged` will only pick up asset changes in schedules +/// which runs in `PostUpdate`. Therefore, `AssetChanged` will only pick up asset changes in schedules /// following [`AssetEventSystems`] or the next frame. Consider adding the system in the `Last` schedule /// after [`AssetEventSystems`] if you need to react without frame delay to asset changes. /// @@ -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/assets.rs b/crates/bevy_asset/src/assets.rs index 9fa8eb4381..6e5b488ee0 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -437,6 +437,18 @@ impl Assets { result } + /// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists. + /// + /// This is the same as [`Assets::get_mut`] except it doesn't emit [`AssetEvent::Modified`]. + #[inline] + pub fn get_mut_untracked(&mut self, id: impl Into>) -> Option<&mut A> { + let id: AssetId = id.into(); + match id { + AssetId::Index { index, .. } => self.dense_storage.get_mut(index), + AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), + } + } + /// Removes (and returns) the [`Asset`] with the given `id`, if it exists. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. pub fn remove(&mut self, id: impl Into>) -> Option { @@ -450,6 +462,8 @@ impl Assets { /// Removes (and returns) the [`Asset`] with the given `id`, if it exists. This skips emitting [`AssetEvent::Removed`]. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. + /// + /// This is the same as [`Assets::remove`] except it doesn't emit [`AssetEvent::Removed`]. pub fn remove_untracked(&mut self, id: impl Into>) -> Option { let id: AssetId = id.into(); self.duplicate_handles.remove(&id); 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/handle.rs b/crates/bevy_asset/src/handle.rs index e6f00c7da5..838c618d8e 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -7,10 +7,12 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; use core::{ any::TypeId, hash::{Hash, Hasher}, + marker::PhantomData, }; use crossbeam_channel::{Receiver, Sender}; use disqualified::ShortName; use thiserror::Error; +use uuid::Uuid; /// Provides [`Handle`] and [`UntypedHandle`] _for a specific asset type_. /// This should _only_ be used for one specific asset type. @@ -117,7 +119,7 @@ impl core::fmt::Debug for StrongHandle { /// avoiding the need to store multiple copies of the same data. /// /// If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept -/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`], +/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Uuid`], it does not necessarily reference a live [`Asset`], /// nor will it keep assets alive. /// /// Modifying a *handle* will change which existing asset is referenced, but modifying the *asset* @@ -133,16 +135,16 @@ pub enum Handle { /// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. Strong(Arc), - /// A "weak" reference to an [`Asset`]. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`], - /// nor will it keep assets alive. - Weak(AssetId), + /// A reference to an [`Asset`] using a stable-across-runs / const identifier. Dropping this + /// handle will not result in the asset being dropped. + Uuid(Uuid, #[reflect(ignore, clone)] PhantomData A>), } impl Clone for Handle { fn clone(&self) -> Self { match self { Handle::Strong(handle) => Handle::Strong(handle.clone()), - Handle::Weak(id) => Handle::Weak(*id), + Handle::Uuid(uuid, ..) => Handle::Uuid(*uuid, PhantomData), } } } @@ -153,7 +155,7 @@ impl Handle { pub fn id(&self) -> AssetId { match self { Handle::Strong(handle) => handle.id.typed_unchecked(), - Handle::Weak(id) => *id, + Handle::Uuid(uuid, ..) => AssetId::Uuid { uuid: *uuid }, } } @@ -162,14 +164,14 @@ impl Handle { pub fn path(&self) -> Option<&AssetPath<'static>> { match self { Handle::Strong(handle) => handle.path.as_ref(), - Handle::Weak(_) => None, + Handle::Uuid(..) => None, } } - /// Returns `true` if this is a weak handle. + /// Returns `true` if this is a uuid handle. #[inline] - pub fn is_weak(&self) -> bool { - matches!(self, Handle::Weak(_)) + pub fn is_uuid(&self) -> bool { + matches!(self, Handle::Uuid(..)) } /// Returns `true` if this is a strong handle. @@ -178,18 +180,9 @@ impl Handle { matches!(self, Handle::Strong(_)) } - /// Creates a [`Handle::Weak`] clone of this [`Handle`], which will not keep the referenced [`Asset`] alive. - #[inline] - pub fn clone_weak(&self) -> Self { - match self { - Handle::Strong(handle) => Handle::Weak(handle.id.typed_unchecked::()), - Handle::Weak(id) => Handle::Weak(*id), - } - } - /// Converts this [`Handle`] to an "untyped" / "generic-less" [`UntypedHandle`], which stores the [`Asset`] type information - /// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Weak`] for - /// [`Handle::Weak`]. + /// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Uuid`] for + /// [`Handle::Uuid`]. #[inline] pub fn untyped(self) -> UntypedHandle { self.into() @@ -198,7 +191,7 @@ impl Handle { impl Default for Handle { fn default() -> Self { - Handle::Weak(AssetId::default()) + Handle::Uuid(AssetId::::DEFAULT_UUID, PhantomData) } } @@ -214,7 +207,7 @@ impl core::fmt::Debug for Handle { handle.path ) } - Handle::Weak(id) => write!(f, "WeakHandle<{name}>({:?})", id.internal()), + Handle::Uuid(uuid, ..) => write!(f, "UuidHandle<{name}>({uuid:?})"), } } } @@ -284,8 +277,13 @@ impl From<&mut Handle> for UntypedAssetId { pub enum UntypedHandle { /// A strong handle, which will keep the referenced [`Asset`] alive until all strong handles are dropped. Strong(Arc), - /// A weak handle, which does not keep the referenced [`Asset`] alive. - Weak(UntypedAssetId), + /// A UUID handle, which does not keep the referenced [`Asset`] alive. + Uuid { + /// An identifier that records the underlying asset type. + type_id: TypeId, + /// The UUID provided during asset registration. + uuid: Uuid, + }, } impl UntypedHandle { @@ -294,7 +292,10 @@ impl UntypedHandle { pub fn id(&self) -> UntypedAssetId { match self { UntypedHandle::Strong(handle) => handle.id, - UntypedHandle::Weak(id) => *id, + UntypedHandle::Uuid { type_id, uuid } => UntypedAssetId::Uuid { + uuid: *uuid, + type_id: *type_id, + }, } } @@ -303,16 +304,7 @@ impl UntypedHandle { pub fn path(&self) -> Option<&AssetPath<'static>> { match self { UntypedHandle::Strong(handle) => handle.path.as_ref(), - UntypedHandle::Weak(_) => None, - } - } - - /// Creates an [`UntypedHandle::Weak`] clone of this [`UntypedHandle`], which will not keep the referenced [`Asset`] alive. - #[inline] - pub fn clone_weak(&self) -> UntypedHandle { - match self { - UntypedHandle::Strong(handle) => UntypedHandle::Weak(handle.id), - UntypedHandle::Weak(id) => UntypedHandle::Weak(*id), + UntypedHandle::Uuid { .. } => None, } } @@ -321,7 +313,7 @@ impl UntypedHandle { pub fn type_id(&self) -> TypeId { match self { UntypedHandle::Strong(handle) => handle.id.type_id(), - UntypedHandle::Weak(id) => id.type_id(), + UntypedHandle::Uuid { type_id, .. } => *type_id, } } @@ -330,7 +322,7 @@ impl UntypedHandle { pub fn typed_unchecked(self) -> Handle { match self { UntypedHandle::Strong(handle) => Handle::Strong(handle), - UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::()), + UntypedHandle::Uuid { uuid, .. } => Handle::Uuid(uuid, PhantomData), } } @@ -345,10 +337,7 @@ impl UntypedHandle { TypeId::of::(), "The target Handle's TypeId does not match the TypeId of this UntypedHandle" ); - match self { - UntypedHandle::Strong(handle) => Handle::Strong(handle), - UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::()), - } + self.typed_unchecked() } /// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A` @@ -376,7 +365,7 @@ impl UntypedHandle { pub fn meta_transform(&self) -> Option<&MetaTransform> { match self { UntypedHandle::Strong(handle) => handle.meta_transform.as_ref(), - UntypedHandle::Weak(_) => None, + UntypedHandle::Uuid { .. } => None, } } } @@ -409,12 +398,9 @@ impl core::fmt::Debug for UntypedHandle { handle.path ) } - UntypedHandle::Weak(id) => write!( - f, - "WeakHandle{{ type_id: {:?}, id: {:?} }}", - id.type_id(), - id.internal() - ), + UntypedHandle::Uuid { type_id, uuid } => { + write!(f, "UuidHandle{{ type_id: {type_id:?}, uuid: {uuid:?} }}",) + } } } } @@ -474,7 +460,10 @@ impl From> for UntypedHandle { fn from(value: Handle) -> Self { match value { Handle::Strong(handle) => UntypedHandle::Strong(handle), - Handle::Weak(id) => UntypedHandle::Weak(id.into()), + Handle::Uuid(uuid, _) => UntypedHandle::Uuid { + type_id: TypeId::of::(), + uuid, + }, } } } @@ -490,36 +479,37 @@ impl TryFrom for Handle { return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found }); } - match value { - UntypedHandle::Strong(handle) => Ok(Handle::Strong(handle)), - UntypedHandle::Weak(id) => { - let Ok(id) = id.try_into() else { - return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found }); - }; - Ok(Handle::Weak(id)) - } - } + Ok(match value { + UntypedHandle::Strong(handle) => Handle::Strong(handle), + UntypedHandle::Uuid { uuid, .. } => Handle::Uuid(uuid, PhantomData), + }) } } -/// Creates a weak [`Handle`] from a string literal containing a UUID. +/// Creates a [`Handle`] from a string literal containing a UUID. /// /// # Examples /// /// ``` -/// # use bevy_asset::{Handle, weak_handle}; -/// # type Shader = (); -/// const SHADER: Handle = weak_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac"); +/// # use bevy_asset::{Handle, uuid_handle}; +/// # type Image = (); +/// const IMAGE: Handle = uuid_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac"); /// ``` #[macro_export] -macro_rules! weak_handle { +macro_rules! uuid_handle { ($uuid:expr) => {{ - $crate::Handle::Weak($crate::AssetId::Uuid { - uuid: $crate::uuid::uuid!($uuid), - }) + $crate::Handle::Uuid($crate::uuid::uuid!($uuid), core::marker::PhantomData) }}; } +#[deprecated = "Use uuid_handle! instead"] +#[macro_export] +macro_rules! weak_handle { + ($uuid:expr) => { + uuid_handle!($uuid) + }; +} + /// Errors preventing the conversion of to/from an [`UntypedHandle`] and a [`Handle`]. #[derive(Error, Debug, PartialEq, Clone)] #[non_exhaustive] @@ -559,15 +549,12 @@ mod tests { /// Typed and Untyped `Handles` should be equivalent to each other and themselves #[test] fn equality() { - let typed = AssetId::::Uuid { uuid: UUID_1 }; - let untyped = UntypedAssetId::Uuid { + let typed = Handle::::Uuid(UUID_1, PhantomData); + let untyped = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let typed = Handle::Weak(typed); - let untyped = UntypedHandle::Weak(untyped); - assert_eq!( Ok(typed.clone()), Handle::::try_from(untyped.clone()) @@ -585,22 +572,17 @@ mod tests { fn ordering() { assert!(UUID_1 < UUID_2); - let typed_1 = AssetId::::Uuid { uuid: UUID_1 }; - let typed_2 = AssetId::::Uuid { uuid: UUID_2 }; - let untyped_1 = UntypedAssetId::Uuid { + let typed_1 = Handle::::Uuid(UUID_1, PhantomData); + let typed_2 = Handle::::Uuid(UUID_2, PhantomData); + let untyped_1 = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let untyped_2 = UntypedAssetId::Uuid { + let untyped_2 = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_2, }; - let typed_1 = Handle::Weak(typed_1); - let typed_2 = Handle::Weak(typed_2); - let untyped_1 = UntypedHandle::Weak(untyped_1); - let untyped_2 = UntypedHandle::Weak(untyped_2); - assert!(typed_1 < typed_2); assert!(untyped_1 < untyped_2); @@ -617,15 +599,12 @@ mod tests { /// Typed and Untyped `Handles` should be equivalently hashable to each other and themselves #[test] fn hashing() { - let typed = AssetId::::Uuid { uuid: UUID_1 }; - let untyped = UntypedAssetId::Uuid { + let typed = Handle::::Uuid(UUID_1, PhantomData); + let untyped = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let typed = Handle::Weak(typed); - let untyped = UntypedHandle::Weak(untyped); - assert_eq!( hash(&typed), hash(&Handle::::try_from(untyped.clone()).unwrap()) @@ -637,15 +616,12 @@ mod tests { /// Typed and Untyped `Handles` should be interchangeable #[test] fn conversion() { - let typed = AssetId::::Uuid { uuid: UUID_1 }; - let untyped = UntypedAssetId::Uuid { + let typed = Handle::::Uuid(UUID_1, PhantomData); + let untyped = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let typed = Handle::Weak(typed); - let untyped = UntypedHandle::Weak(untyped); - assert_eq!(typed, Handle::try_from(untyped.clone()).unwrap()); assert_eq!(UntypedHandle::from(typed.clone()), untyped); } 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/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index c2551a40f1..4ed7162d2b 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -47,7 +47,7 @@ fn js_value_to_err(context: &str) -> impl FnOnce(JsValue) -> std::io::Error + '_ } }; - std::io::Error::new(std::io::ErrorKind::Other, message) + std::io::Error::other(message) } } @@ -62,10 +62,7 @@ impl HttpWasmAssetReader { let worker: web_sys::WorkerGlobalScope = global.unchecked_into(); worker.fetch_with_str(path.to_str().unwrap()) } else { - let error = std::io::Error::new( - std::io::ErrorKind::Other, - "Unsupported JavaScript global context", - ); + let error = std::io::Error::other("Unsupported JavaScript global context"); return Err(AssetReaderError::Io(error.into())); }; let resp_value = JsFuture::from(promise) @@ -81,7 +78,10 @@ impl HttpWasmAssetReader { let reader = VecReader::new(bytes); Ok(reader) } - 404 => Err(AssetReaderError::NotFound(path)), + // Some web servers, including itch.io's CDN, return 403 when a requested file isn't present. + // TODO: remove handling of 403 as not found when it's easier to configure + // see https://github.com/bevyengine/bevy/pull/19268#pullrequestreview-2882410105 + 403 | 404 => Err(AssetReaderError::NotFound(path)), status => Err(AssetReaderError::HttpError(status)), } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 5b680eb191..8186b6315d 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -141,8 +141,8 @@ #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -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, } @@ -2000,4 +2000,92 @@ mod tests { app.world_mut().run_schedule(Update); } + + #[test] + #[ignore = "blocked on https://github.com/bevyengine/bevy/issues/11111"] + fn same_asset_different_settings() { + // Test loading the same asset twice with different settings. This should + // produce two distinct assets. + + // First, implement an asset that's a single u8, whose value is copied from + // the loader settings. + + #[derive(Asset, TypePath)] + struct U8Asset(u8); + + #[derive(Serialize, Deserialize, Default)] + struct U8LoaderSettings(u8); + + struct U8Loader; + + impl AssetLoader for U8Loader { + type Asset = U8Asset; + type Settings = U8LoaderSettings; + type Error = crate::loader::LoadDirectError; + + async fn load( + &self, + _: &mut dyn Reader, + settings: &Self::Settings, + _: &mut LoadContext<'_>, + ) -> Result { + Ok(U8Asset(settings.0)) + } + + fn extensions(&self) -> &[&str] { + &["u8"] + } + } + + // Create a test asset. + + let dir = Dir::default(); + dir.insert_asset(Path::new("test.u8"), &[]); + + let asset_source = AssetSource::build() + .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() })); + + // Set up the app. + + let mut app = App::new(); + + app.register_asset_source(AssetSourceId::Default, asset_source) + .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) + .init_asset::() + .register_asset_loader(U8Loader); + + let asset_server = app.world().resource::(); + + // Load the test asset twice but with different settings. + + fn load(asset_server: &AssetServer, path: &str, value: u8) -> Handle { + asset_server.load_with_settings::( + path, + move |s: &mut U8LoaderSettings| s.0 = value, + ) + } + + let handle_1 = load(asset_server, "test.u8", 1); + let handle_2 = load(asset_server, "test.u8", 2); + + // Handles should be different. + + assert_ne!(handle_1, handle_2); + + run_app_until(&mut app, |world| { + let (Some(asset_1), Some(asset_2)) = ( + world.resource::>().get(&handle_1), + world.resource::>().get(&handle_2), + ) else { + return None; + }; + + // Values should match the settings. + + assert_eq!(asset_1.0, 1); + assert_eq!(asset_2.0, 2); + + Some(()) + }); + } } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 8f4863b885..24405f0657 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -12,7 +12,7 @@ use alloc::{ vec::Vec, }; use atomicow::CowArc; -use bevy_ecs::world::World; +use bevy_ecs::{error::BevyError, world::World}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::any::{Any, TypeId}; @@ -34,7 +34,7 @@ pub trait AssetLoader: Send + Sync + 'static { /// The settings type used by this [`AssetLoader`]. type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>; /// The type of [error](`std::error::Error`) which could be encountered by this loader. - type Error: Into>; + type Error: Into; /// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`]. fn load( &self, @@ -58,10 +58,7 @@ pub trait ErasedAssetLoader: Send + Sync + 'static { reader: &'a mut dyn Reader, meta: &'a dyn AssetMetaDyn, load_context: LoadContext<'a>, - ) -> BoxedFuture< - 'a, - Result>, - >; + ) -> BoxedFuture<'a, Result>; /// Returns a list of extensions supported by this asset loader, without the preceding dot. fn extensions(&self) -> &[&str]; @@ -89,10 +86,7 @@ where reader: &'a mut dyn Reader, meta: &'a dyn AssetMetaDyn, mut load_context: LoadContext<'a>, - ) -> BoxedFuture< - 'a, - Result>, - > { + ) -> BoxedFuture<'a, Result> { Box::pin(async move { let settings = meta .loader_settings() @@ -350,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. @@ -366,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())) /// })); @@ -391,18 +385,18 @@ 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( + pub fn labeled_asset_scope( &mut self, label: String, - load: impl FnOnce(&mut LoadContext) -> A, - ) -> Handle { + load: impl FnOnce(&mut LoadContext) -> Result, + ) -> Result, E> { let mut context = self.begin_labeled_asset(); - let asset = load(&mut context); + let asset = load(&mut context)?; let loaded_asset = context.finish(asset); - self.add_loaded_labeled_asset(label, loaded_asset) + Ok(self.add_loaded_labeled_asset(label, loaded_asset)) } /// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label. @@ -416,7 +410,8 @@ impl<'a> LoadContext<'a> { /// /// See [`AssetPath`] for more on labeled assets. pub fn add_labeled_asset(&mut self, label: String, asset: A) -> Handle { - self.labeled_asset_scope(label, |_| asset) + self.labeled_asset_scope(label, |_| Ok::<_, ()>(asset)) + .expect("the closure returns Ok") } /// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context. diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index 97e6c6499d..ed189a683b 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -480,7 +480,7 @@ impl<'a> AssetPath<'a> { } pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator { - full_extension.chars().enumerate().filter_map(|(i, c)| { + full_extension.char_indices().filter_map(|(i, c)| { if c == '.' { Some(&full_extension[i + 1..]) } else { @@ -490,7 +490,7 @@ impl<'a> AssetPath<'a> { } /// Returns `true` if this [`AssetPath`] points to a file that is - /// outside of it's [`AssetSource`](crate::io::AssetSource) folder. + /// outside of its [`AssetSource`](crate::io::AssetSource) folder. /// /// ## Example /// ``` 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 ff5800474d..69dc8428da 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -167,7 +167,7 @@ impl AssetServer { fn sender(world: &mut World, id: UntypedAssetId) { world .resource_mut::>>() - .send(AssetEvent::LoadedWithDependencies { id: id.typed() }); + .write(AssetEvent::LoadedWithDependencies { id: id.typed() }); } fn failed_sender( world: &mut World, @@ -177,7 +177,7 @@ impl AssetServer { ) { world .resource_mut::>>() - .send(AssetLoadFailedEvent { + .write(AssetLoadFailedEvent { id: id.typed(), path, error, @@ -553,7 +553,9 @@ impl AssetServer { path: impl Into>, ) -> Result { let path: AssetPath = path.into(); - self.load_internal(None, path, false, None).await + self.load_internal(None, path, false, None) + .await + .map(|h| h.expect("handle must be returned, since we didn't pass in an input handle")) } pub(crate) fn load_unknown_type_with_meta_transform<'a>( @@ -643,21 +645,25 @@ impl AssetServer { /// Performs an async asset load. /// - /// `input_handle` must only be [`Some`] if `should_load` was true when retrieving `input_handle`. This is an optimization to - /// avoid looking up `should_load` twice, but it means you _must_ be sure a load is necessary when calling this function with [`Some`]. + /// `input_handle` must only be [`Some`] if `should_load` was true when retrieving + /// `input_handle`. This is an optimization to avoid looking up `should_load` twice, but it + /// means you _must_ be sure a load is necessary when calling this function with [`Some`]. + /// + /// Returns the handle of the asset if one was retrieved by this function. Otherwise, may return + /// [`None`]. async fn load_internal<'a>( &self, - mut input_handle: Option, + input_handle: Option, path: AssetPath<'a>, force: bool, meta_transform: Option, - ) -> Result { - let asset_type_id = input_handle.as_ref().map(UntypedHandle::type_id); + ) -> Result, AssetLoadError> { + let input_handle_type_id = input_handle.as_ref().map(UntypedHandle::type_id); let path = path.into_owned(); let path_clone = path.clone(); let (mut meta, loader, mut reader) = self - .get_meta_loader_and_reader(&path_clone, asset_type_id) + .get_meta_loader_and_reader(&path_clone, input_handle_type_id) .await .inspect_err(|e| { // if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if @@ -674,76 +680,90 @@ impl AssetServer { if let Some(meta_transform) = input_handle.as_ref().and_then(|h| h.meta_transform()) { (*meta_transform)(&mut *meta); } - // downgrade the input handle so we don't keep the asset alive just because we're loading it - // note we can't just pass a weak handle in, as only strong handles contain the asset meta transform - input_handle = input_handle.map(|h| h.clone_weak()); - // This contains Some(UntypedHandle), if it was retrievable - // If it is None, that is because it was _not_ retrievable, due to - // 1. The handle was not already passed in for this path, meaning we can't just use that - // 2. The asset has not been loaded yet, meaning there is no existing Handle for it - // 3. The path has a label, meaning the AssetLoader's root asset type is not the path's asset type - // - // In the None case, the only course of action is to wait for the asset to load so we can allocate the - // handle for that type. - // - // TODO: Note that in the None case, multiple asset loads for the same path can happen at the same time - // (rather than "early out-ing" in the "normal" case) - // This would be resolved by a universal asset id, as we would not need to resolve the asset type - // to generate the ID. See this issue: https://github.com/bevyengine/bevy/issues/10549 - let handle_result = match input_handle { - Some(handle) => { - // if a handle was passed in, the "should load" check was already done - Some((handle, true)) - } - None => { - let mut infos = self.data.infos.write(); - let result = infos.get_or_create_path_handle_internal( - path.clone(), - path.label().is_none().then(|| loader.asset_type_id()), - HandleLoadingMode::Request, - meta_transform, - ); - unwrap_with_context(result, Either::Left(loader.asset_type_name())) - } - }; + let asset_id; // The asset ID of the asset we are trying to load. + let fetched_handle; // The handle if one was looked up/created. + let should_load; // Whether we need to load the asset. + if let Some(input_handle) = input_handle { + asset_id = Some(input_handle.id()); + // In this case, we intentionally drop the input handle so we can cancel loading the + // asset if the handle gets dropped (externally) before it finishes loading. + fetched_handle = None; + // The handle was passed in, so the "should_load" check was already done. + should_load = true; + } else { + // TODO: multiple asset loads for the same path can happen at the same time (rather than + // "early out-ing" in the "normal" case). This would be resolved by a universal asset + // id, as we would not need to resolve the asset type to generate the ID. See this + // issue: https://github.com/bevyengine/bevy/issues/10549 - let handle = if let Some((handle, should_load)) = handle_result { - if path.label().is_none() && handle.type_id() != loader.asset_type_id() { + let mut infos = self.data.infos.write(); + let result = infos.get_or_create_path_handle_internal( + path.clone(), + path.label().is_none().then(|| loader.asset_type_id()), + HandleLoadingMode::Request, + meta_transform, + ); + match unwrap_with_context(result, Either::Left(loader.asset_type_name())) { + // We couldn't figure out the correct handle without its type ID (which can only + // happen if we are loading a subasset). + None => { + // We don't know the expected type since the subasset may have a different type + // than the "root" asset (which is the type the loader will load). + asset_id = None; + fetched_handle = None; + // If we couldn't find an appropriate handle, then the asset certainly needs to + // be loaded. + should_load = true; + } + Some((handle, result_should_load)) => { + asset_id = Some(handle.id()); + fetched_handle = Some(handle); + should_load = result_should_load; + } + } + } + // Verify that the expected type matches the loader's type. + if let Some(asset_type_id) = asset_id.map(|id| id.type_id()) { + // If we are loading a subasset, then the subasset's type almost certainly doesn't match + // the loader's type - and that's ok. + if path.label().is_none() && asset_type_id != loader.asset_type_id() { error!( "Expected {:?}, got {:?}", - handle.type_id(), + asset_type_id, loader.asset_type_id() ); return Err(AssetLoadError::RequestedHandleTypeMismatch { path: path.into_owned(), - requested: handle.type_id(), + requested: asset_type_id, actual_asset_name: loader.asset_type_name(), loader_name: loader.type_name(), }); } - if !should_load && !force { - return Ok(handle); - } - Some(handle) - } else { - None - }; - // if the handle result is None, we definitely need to load the asset + } + // Bail out earlier if we don't need to load the asset. + if !should_load && !force { + return Ok(fetched_handle); + } - let (base_handle, base_path) = if path.label().is_some() { + // We don't actually need to use _base_handle, but we do need to keep the handle alive. + // Dropping it would cancel the load of the base asset, which would make the load of this + // subasset never complete. + let (base_asset_id, _base_handle, base_path) = if path.label().is_some() { let mut infos = self.data.infos.write(); let base_path = path.without_label().into_owned(); - let (base_handle, _) = infos.get_or_create_path_handle_erased( - base_path.clone(), - loader.asset_type_id(), - Some(loader.asset_type_name()), - HandleLoadingMode::Force, - None, - ); - (base_handle, base_path) + let base_handle = infos + .get_or_create_path_handle_erased( + base_path.clone(), + loader.asset_type_id(), + Some(loader.asset_type_name()), + HandleLoadingMode::Force, + None, + ) + .0; + (base_handle.id(), Some(base_handle), base_path) } else { - (handle.clone().unwrap(), path.clone()) + (asset_id.unwrap(), None, path.clone()) }; match self @@ -760,7 +780,7 @@ impl AssetServer { Ok(loaded_asset) => { let final_handle = if let Some(label) = path.label_cow() { match loaded_asset.labeled_assets.get(&label) { - Some(labeled_asset) => labeled_asset.handle.clone(), + Some(labeled_asset) => Some(labeled_asset.handle.clone()), None => { let mut all_labels: Vec = loaded_asset .labeled_assets @@ -776,16 +796,15 @@ impl AssetServer { } } } else { - // if the path does not have a label, the handle must exist at this point - handle.unwrap() + fetched_handle }; - self.send_loaded_asset(base_handle.id(), loaded_asset); + self.send_loaded_asset(base_asset_id, loaded_asset); Ok(final_handle) } Err(err) => { self.send_asset_event(InternalAssetEvent::Failed { - id: base_handle.id(), + id: base_asset_id, error: err.clone(), path: path.into_owned(), }); @@ -1666,7 +1685,7 @@ pub fn handle_internal_asset_events(world: &mut World) { } if !untyped_failures.is_empty() { - world.send_event_batch(untyped_failures); + world.write_event_batch(untyped_failures); } fn queue_ancestors( @@ -1931,7 +1950,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, @@ -1945,7 +1964,7 @@ pub enum AssetLoadError { pub struct AssetLoaderError { path: AssetPath<'static>, loader_name: &'static str, - error: Arc, + error: Arc, } impl AssetLoaderError { @@ -1953,6 +1972,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 84060fe26b..8beba77c0d 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -1,39 +1,44 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" 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" } # 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.rs b/crates/bevy_audio/src/audio.rs index 349cf6b6a4..0407997565 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -3,6 +3,7 @@ use bevy_asset::{Asset, Handle}; use bevy_ecs::prelude::*; use bevy_math::Vec3; use bevy_reflect::prelude::*; +use bevy_transform::components::Transform; /// The way Bevy manages the sound playback. #[derive(Debug, Clone, Copy, Reflect)] @@ -10,10 +11,10 @@ use bevy_reflect::prelude::*; pub enum PlaybackMode { /// Play the sound once. Do nothing when it ends. /// - /// Note: It is not possible to reuse an `AudioPlayer` after it has finished playing and - /// the underlying `AudioSink` or `SpatialAudioSink` has been drained. + /// Note: It is not possible to reuse an [`AudioPlayer`] after it has finished playing and + /// the underlying [`AudioSink`](crate::AudioSink) or [`SpatialAudioSink`](crate::SpatialAudioSink) has been drained. /// - /// To replay a sound, the audio components provided by `AudioPlayer` must be removed and + /// To replay a sound, the audio components provided by [`AudioPlayer`] must be removed and /// added again. Once, /// Repeat the sound forever. @@ -27,7 +28,7 @@ pub enum PlaybackMode { /// Initial settings to be used when audio starts playing. /// /// If you would like to control the audio while it is playing, query for the -/// [`AudioSink`][crate::AudioSink] or [`SpatialAudioSink`][crate::SpatialAudioSink] +/// [`AudioSink`](crate::AudioSink) or [`SpatialAudioSink`](crate::SpatialAudioSink) /// components. Changes to this component will *not* be applied to already-playing audio. #[derive(Component, Clone, Copy, Debug, Reflect)] #[reflect(Clone, Default, Component, Debug)] @@ -57,6 +58,16 @@ pub struct PlaybackSettings { /// Optional scale factor applied to the positions of this audio source and the listener, /// overriding the default value configured on [`AudioPlugin::default_spatial_scale`](crate::AudioPlugin::default_spatial_scale). pub spatial_scale: Option, + /// The point in time in the audio clip where playback should start. If set to `None`, it will + /// play from the beginning of the clip. + /// + /// If the playback mode is set to `Loop`, each loop will start from this position. + pub start_position: Option, + /// How long the audio should play before stopping. If set, the clip will play for at most + /// the specified duration. If set to `None`, it will play for as long as it can. + /// + /// If the playback mode is set to `Loop`, each loop will last for this duration. + pub duration: Option, } impl Default for PlaybackSettings { @@ -68,10 +79,10 @@ impl Default for PlaybackSettings { impl PlaybackSettings { /// Will play the associated audio source once. /// - /// Note: It is not possible to reuse an `AudioPlayer` after it has finished playing and - /// the underlying `AudioSink` or `SpatialAudioSink` has been drained. + /// Note: It is not possible to reuse an [`AudioPlayer`] after it has finished playing and + /// the underlying [`AudioSink`](crate::AudioSink) or [`SpatialAudioSink`](crate::SpatialAudioSink) has been drained. /// - /// To replay a sound, the audio components provided by `AudioPlayer` must be removed and + /// To replay a sound, the audio components provided by [`AudioPlayer`] must be removed and /// added again. pub const ONCE: PlaybackSettings = PlaybackSettings { mode: PlaybackMode::Once, @@ -81,6 +92,8 @@ impl PlaybackSettings { muted: false, spatial: false, spatial_scale: None, + start_position: None, + duration: None, }; /// Will play the associated audio source in a loop. @@ -136,18 +149,31 @@ impl PlaybackSettings { self.spatial_scale = Some(spatial_scale); self } + + /// Helper to use a custom playback start position. + pub const fn with_start_position(mut self, start_position: core::time::Duration) -> Self { + self.start_position = Some(start_position); + self + } + + /// Helper to use a custom playback duration. + pub const fn with_duration(mut self, duration: core::time::Duration) -> Self { + self.duration = Some(duration); + self + } } /// Settings for the listener for spatial audio sources. /// -/// This must be accompanied by `Transform` and `GlobalTransform`. -/// Only one entity with a `SpatialListener` should be present at any given time. +/// This is accompanied by [`Transform`] and [`GlobalTransform`](bevy_transform::prelude::GlobalTransform). +/// Only one entity with a [`SpatialListener`] should be present at any given time. #[derive(Component, Clone, Debug, Reflect)] +#[require(Transform)] #[reflect(Clone, Default, Component, Debug)] pub struct SpatialListener { - /// Left ear position relative to the `GlobalTransform`. + /// Left ear position relative to the [`GlobalTransform`](bevy_transform::prelude::GlobalTransform). pub left_ear_offset: Vec3, - /// Right ear position relative to the `GlobalTransform`. + /// Right ear position relative to the [`GlobalTransform`](bevy_transform::prelude::GlobalTransform). pub right_ear_offset: Vec3, } @@ -158,7 +184,7 @@ impl Default for SpatialListener { } impl SpatialListener { - /// Creates a new `SpatialListener` component. + /// Creates a new [`SpatialListener`] component. /// /// `gap` is the distance between the left and right "ears" of the listener. Ears are /// positioned on the x axis. @@ -179,12 +205,12 @@ impl SpatialListener { pub struct SpatialScale(pub Vec3); impl SpatialScale { - /// Create a new `SpatialScale` with the same value for all 3 dimensions. + /// Create a new [`SpatialScale`] with the same value for all 3 dimensions. pub const fn new(scale: f32) -> Self { Self(Vec3::splat(scale)) } - /// Create a new `SpatialScale` with the same value for `x` and `y`, and `0.0` + /// Create a new [`SpatialScale`] with the same value for `x` and `y`, and `0.0` /// for `z`. pub const fn new_2d(scale: f32) -> Self { Self(Vec3::new(scale, scale, 0.0)) @@ -214,11 +240,11 @@ pub struct DefaultSpatialScale(pub SpatialScale); /// If the handle refers to an unavailable asset (such as if it has not finished loading yet), /// the audio will not begin playing immediately. The audio will play when the asset is ready. /// -/// When Bevy begins the audio playback, an [`AudioSink`][crate::AudioSink] component will be +/// When Bevy begins the audio playback, an [`AudioSink`](crate::AudioSink) component will be /// added to the entity. You can use that component to control the audio settings during playback. /// /// Playback can be configured using the [`PlaybackSettings`] component. Note that changes to the -/// `PlaybackSettings` component will *not* affect already-playing audio. +/// [`PlaybackSettings`] component will *not* affect already-playing audio. #[derive(Component, Reflect)] #[reflect(Component, Clone)] #[require(PlaybackSettings)] diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index 1869fb4755..749b08a3b9 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. /// @@ -102,7 +103,7 @@ pub(crate) fn play_queued_audio_system( Entity, &AudioPlayer, &PlaybackSettings, - Option<&GlobalTransform>, + &GlobalTransform, ), (Without, Without), >, @@ -117,7 +118,7 @@ pub(crate) fn play_queued_audio_system( return; }; - for (entity, source_handle, settings, maybe_emitter_transform) in &query_nonplaying { + for (entity, source_handle, settings, emitter_transform) in &query_nonplaying { let Some(audio_source) = audio_sources.get(&source_handle.0) else { continue; }; @@ -135,14 +136,7 @@ pub(crate) fn play_queued_audio_system( } let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0; - - let emitter_translation = if let Some(emitter_transform) = maybe_emitter_transform { - (emitter_transform.translation() * scale).into() - } else { - warn!("Spatial AudioPlayer with no GlobalTransform component. Using zero."); - Vec3::ZERO.into() - }; - + let emitter_translation = (emitter_transform.translation() * scale).into(); let sink = match SpatialSink::try_new( stream_handle, emitter_translation, @@ -156,12 +150,49 @@ pub(crate) fn play_queued_audio_system( } }; + let decoder = audio_source.decoder(); + match settings.mode { - PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), + PlaybackMode::Loop => match (settings.start_position, settings.duration) { + // custom start position and duration + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration) + .repeat_infinite(), + ), + + // custom start position + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position).repeat_infinite()); + } + + // custom duration + (None, Some(duration)) => { + sink.append(decoder.take_duration(duration).repeat_infinite()); + } + + // full clip + (None, None) => sink.append(decoder.repeat_infinite()), + }, PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { - sink.append(audio_source.decoder()); + match (settings.start_position, settings.duration) { + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration), + ), + + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position)); + } + + (None, Some(duration)) => sink.append(decoder.take_duration(duration)), + + (None, None) => sink.append(decoder), + } } - }; + } let mut sink = SpatialAudioSink::new(sink); @@ -196,12 +227,49 @@ pub(crate) fn play_queued_audio_system( } }; + let decoder = audio_source.decoder(); + match settings.mode { - PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), + PlaybackMode::Loop => match (settings.start_position, settings.duration) { + // custom start position and duration + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration) + .repeat_infinite(), + ), + + // custom start position + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position).repeat_infinite()); + } + + // custom duration + (None, Some(duration)) => { + sink.append(decoder.take_duration(duration).repeat_infinite()); + } + + // full clip + (None, None) => sink.append(decoder.repeat_infinite()), + }, PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { - sink.append(audio_source.decoder()); + match (settings.start_position, settings.duration) { + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration), + ), + + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position)); + } + + (None, Some(duration)) => sink.append(decoder.take_duration(duration)), + + (None, None) => sink.append(decoder), + } } - }; + } let mut sink = AudioSink::new(sink); diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index becbf5d1da..e3b5e02569 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Audio support for the game engine Bevy diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index ed51754f86..1e020d1fd8 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -42,6 +42,14 @@ pub trait AudioSinkPlayback { /// No effect if not paused. fn play(&self); + /// Returns the position of the sound that's being played. + /// + /// This takes into account any speedup or delay applied. + /// + /// Example: if you [`set_speed(2.0)`](Self::set_speed) and [`position()`](Self::position) returns *5s*, + /// then the position in the recording is *10s* from its start. + fn position(&self) -> Duration; + /// Attempts to seek to a given position in the current source. /// /// This blocks between 0 and ~5 milliseconds. @@ -181,6 +189,10 @@ impl AudioSinkPlayback for AudioSink { self.sink.play(); } + fn position(&self) -> Duration { + self.sink.get_pos() + } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { self.sink.try_seek(pos) } @@ -281,6 +293,10 @@ impl AudioSinkPlayback for SpatialAudioSink { self.sink.play(); } + fn position(&self) -> Duration { + self.sink.get_pos() + } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { self.sink.try_seek(pos) } diff --git a/crates/bevy_audio/src/volume.rs b/crates/bevy_audio/src/volume.rs index b1378ae485..b8c0c776a5 100644 --- a/crates/bevy_audio/src/volume.rs +++ b/crates/bevy_audio/src/volume.rs @@ -34,7 +34,7 @@ impl GlobalVolume { #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Clone, Debug, PartialEq)] pub enum Volume { - /// Create a new [`Volume`] from the given volume in linear scale. + /// Create a new [`Volume`] from the given volume in the linear scale. /// /// In a linear scale, the value `1.0` represents the "normal" volume, /// meaning the audio is played at its original level. Values greater than @@ -144,7 +144,7 @@ impl Volume { /// Returns the volume in decibels as a float. /// - /// If the volume is silent / off / muted, i.e. it's underlying linear scale + /// If the volume is silent / off / muted, i.e., its underlying linear scale /// is `0.0`, this method returns negative infinity. pub fn to_decibels(&self) -> f32 { match self { @@ -155,57 +155,95 @@ impl Volume { /// The silent volume. Also known as "off" or "muted". pub const SILENT: Self = Volume::Linear(0.0); -} -impl core::ops::Add for Volume { - type Output = Self; - - fn add(self, rhs: Self) -> Self { - use Volume::{Decibels, Linear}; - - match (self, rhs) { - (Linear(a), Linear(b)) => Linear(a + b), - (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( - decibels_to_linear(a) + decibels_to_linear(b), - )), - // {Linear, Decibels} favors the left hand side of the operation by - // first converting the right hand side to the same type as the left - // hand side and then performing the operation. - (Linear(..), Decibels(db)) => self + Linear(decibels_to_linear(db)), - (Decibels(..), Linear(l)) => self + Decibels(linear_to_decibels(l)), - } + /// Increases the volume by the specified percentage. + /// + /// This method works in the linear domain, where a 100% increase + /// means doubling the volume (equivalent to +6.02dB). + /// + /// # Arguments + /// * `percentage` - The percentage to increase (50.0 means 50% increase) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let volume = Volume::Linear(1.0); + /// let increased = volume.increase_by_percentage(100.0); + /// assert_eq!(increased.to_linear(), 2.0); + /// ``` + pub fn increase_by_percentage(&self, percentage: f32) -> Self { + let factor = 1.0 + (percentage / 100.0); + Volume::Linear(self.to_linear() * factor) } -} -impl core::ops::AddAssign for Volume { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; + /// Decreases the volume by the specified percentage. + /// + /// This method works in the linear domain, where a 50% decrease + /// means halving the volume (equivalent to -6.02dB). + /// + /// # Arguments + /// * `percentage` - The percentage to decrease (50.0 means 50% decrease) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let volume = Volume::Linear(1.0); + /// let decreased = volume.decrease_by_percentage(50.0); + /// assert_eq!(decreased.to_linear(), 0.5); + /// ``` + pub fn decrease_by_percentage(&self, percentage: f32) -> Self { + let factor = 1.0 - (percentage / 100.0).clamp(0.0, 1.0); + Volume::Linear(self.to_linear() * factor) } -} -impl core::ops::Sub for Volume { - type Output = Self; - - fn sub(self, rhs: Self) -> Self { - use Volume::{Decibels, Linear}; - - match (self, rhs) { - (Linear(a), Linear(b)) => Linear(a - b), - (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( - decibels_to_linear(a) - decibels_to_linear(b), - )), - // {Linear, Decibels} favors the left hand side of the operation by - // first converting the right hand side to the same type as the left - // hand side and then performing the operation. - (Linear(..), Decibels(db)) => self - Linear(decibels_to_linear(db)), - (Decibels(..), Linear(l)) => self - Decibels(linear_to_decibels(l)), - } + /// Scales the volume to a specific linear factor relative to the current volume. + /// + /// This is different from `adjust_by_linear` as it sets the volume to be + /// exactly the factor times the original volume, rather than applying + /// the factor to the current volume. + /// + /// # Arguments + /// * `factor` - The scaling factor (2.0 = twice as loud, 0.5 = half as loud) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let volume = Volume::Linear(0.8); + /// let scaled = volume.scale_to_factor(1.25); + /// assert_eq!(scaled.to_linear(), 1.0); + /// ``` + pub fn scale_to_factor(&self, factor: f32) -> Self { + Volume::Linear(self.to_linear() * factor) } -} -impl core::ops::SubAssign for Volume { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; + /// Creates a fade effect by interpolating between current volume and target volume. + /// + /// This method performs linear interpolation in the linear domain, which + /// provides a more natural-sounding fade effect. + /// + /// # Arguments + /// * `target` - The target volume to fade towards + /// * `factor` - The interpolation factor (0.0 = current volume, 1.0 = target volume) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let current = Volume::Linear(1.0); + /// let target = Volume::Linear(0.0); + /// let faded = current.fade_towards(target, 0.5); + /// assert_eq!(faded.to_linear(), 0.5); + /// ``` + pub fn fade_towards(&self, target: Volume, factor: f32) -> Self { + let current_linear = self.to_linear(); + let target_linear = target.to_linear(); + let factor_clamped = factor.clamp(0.0, 1.0); + + let interpolated = current_linear + (target_linear - current_linear) * factor_clamped; + Volume::Linear(interpolated) } } @@ -306,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})", ); } } @@ -337,8 +369,9 @@ mod tests { Linear(f32::NEG_INFINITY).to_decibels().is_infinite(), "Negative infinite linear scale is equivalent to infinite decibels" ); - assert!( - Decibels(f32::NEG_INFINITY).to_linear().abs() == 0.0, + assert_eq!( + Decibels(f32::NEG_INFINITY).to_linear().abs(), + 0.0, "Negative infinity decibels is equivalent to zero linear scale" ); @@ -361,71 +394,89 @@ mod tests { ); } + #[test] + fn test_increase_by_percentage() { + let volume = Linear(1.0); + + // 100% increase should double the volume + let increased = volume.increase_by_percentage(100.0); + assert_eq!(increased.to_linear(), 2.0); + + // 50% increase + let increased = volume.increase_by_percentage(50.0); + assert_eq!(increased.to_linear(), 1.5); + } + + #[test] + fn test_decrease_by_percentage() { + let volume = Linear(1.0); + + // 50% decrease should halve the volume + let decreased = volume.decrease_by_percentage(50.0); + assert_eq!(decreased.to_linear(), 0.5); + + // 25% decrease + let decreased = volume.decrease_by_percentage(25.0); + assert_eq!(decreased.to_linear(), 0.75); + + // 100% decrease should result in silence + let decreased = volume.decrease_by_percentage(100.0); + assert_eq!(decreased.to_linear(), 0.0); + } + + #[test] + fn test_scale_to_factor() { + let volume = Linear(0.8); + let scaled = volume.scale_to_factor(1.25); + assert_eq!(scaled.to_linear(), 1.0); + } + + #[test] + fn test_fade_towards() { + let current = Linear(1.0); + let target = Linear(0.0); + + // 50% fade should result in 0.5 linear volume + let faded = current.fade_towards(target, 0.5); + assert_eq!(faded.to_linear(), 0.5); + + // 0% fade should keep current volume + let faded = current.fade_towards(target, 0.0); + assert_eq!(faded.to_linear(), 1.0); + + // 100% fade should reach target volume + let faded = current.fade_towards(target, 1.0); + assert_eq!(faded.to_linear(), 0.0); + } + + #[test] + fn test_decibel_math_properties() { + let volume = Linear(1.0); + + // Adding 20dB should multiply linear volume by 10 + let adjusted = volume * Decibels(20.0); + assert_approx_eq(adjusted, Linear(10.0)); + + // Subtracting 20dB should divide linear volume by 10 + let adjusted = volume / Decibels(20.0); + assert_approx_eq(adjusted, Linear(0.1)); + } + fn assert_approx_eq(a: Volume, b: Volume) { const EPSILON: f32 = 0.0001; 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:?}", ), } } - #[test] - fn volume_ops_add() { - // Linear to Linear. - assert_approx_eq(Linear(0.5) + Linear(0.5), Linear(1.0)); - assert_approx_eq(Linear(0.5) + Linear(0.1), Linear(0.6)); - assert_approx_eq(Linear(0.5) + Linear(-0.5), Linear(0.0)); - - // Decibels to Decibels. - assert_approx_eq(Decibels(0.0) + Decibels(0.0), Decibels(6.0206003)); - assert_approx_eq(Decibels(6.0) + Decibels(6.0), Decibels(12.020599)); - assert_approx_eq(Decibels(-6.0) + Decibels(-6.0), Decibels(0.020599423)); - - // {Linear, Decibels} favors the left hand side of the operation. - assert_approx_eq(Linear(0.5) + Decibels(0.0), Linear(1.5)); - assert_approx_eq(Decibels(0.0) + Linear(0.5), Decibels(3.521825)); - } - - #[test] - fn volume_ops_add_assign() { - // Linear to Linear. - let mut volume = Linear(0.5); - volume += Linear(0.5); - assert_approx_eq(volume, Linear(1.0)); - } - - #[test] - fn volume_ops_sub() { - // Linear to Linear. - assert_approx_eq(Linear(0.5) - Linear(0.5), Linear(0.0)); - assert_approx_eq(Linear(0.5) - Linear(0.1), Linear(0.4)); - assert_approx_eq(Linear(0.5) - Linear(-0.5), Linear(1.0)); - - // Decibels to Decibels. - assert_eq!(Decibels(0.0) - Decibels(0.0), Decibels(f32::NEG_INFINITY)); - assert_approx_eq(Decibels(6.0) - Decibels(4.0), Decibels(-7.736506)); - assert_eq!(Decibels(-6.0) - Decibels(-6.0), Decibels(f32::NEG_INFINITY)); - } - - #[test] - fn volume_ops_sub_assign() { - // Linear to Linear. - let mut volume = Linear(0.5); - volume -= Linear(0.5); - assert_approx_eq(volume, Linear(0.0)); - } - #[test] fn volume_ops_mul() { // Linear to Linear. diff --git a/crates/bevy_camera/Cargo.toml b/crates/bevy_camera/Cargo.toml new file mode 100644 index 0000000000..6ed3998a82 --- /dev/null +++ b/crates/bevy_camera/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "bevy_camera" +version = "0.17.0-dev" +edition = "2024" +description = "Provides a camera abstraction for Bevy Engine" +homepage = "https://bevy.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_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", 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_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ + "serialize", +] } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } + +# other +wgpu-types = { version = "25", default-features = false } +serde = { version = "1", default-features = false, features = ["derive"] } +thiserror = { version = "2", default-features = false } +downcast-rs = { version = "2", default-features = false, features = ["std"] } +derive_more = { version = "2", default-features = false, features = ["from"] } +smallvec = { version = "1", default-features = false, features = ["const_new"] } + +[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_camera/LICENSE-APACHE b/crates/bevy_camera/LICENSE-APACHE new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/crates/bevy_camera/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_camera/LICENSE-MIT b/crates/bevy_camera/LICENSE-MIT new file mode 100644 index 0000000000..9cf106272a --- /dev/null +++ b/crates/bevy_camera/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_render/src/camera/camera.rs b/crates/bevy_camera/src/camera.rs similarity index 56% rename from crates/bevy_render/src/camera/camera.rs rename to crates/bevy_camera/src/camera.rs index 95218b7a59..a70cbeb39e 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -1,52 +1,21 @@ -#![expect( - clippy::module_inception, - reason = "The parent module contains all things viewport-related, while this module handles cameras as a component. However, a rename/refactor which should clear up this lint is being discussed; see #17196." -)] -use super::{ClearColorConfig, Projection}; -use crate::{ - batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, - camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews}, - primitives::Frustum, - render_asset::RenderAssets, - render_graph::{InternedRenderSubGraph, RenderSubGraph}, - render_resource::TextureView, - sync_world::{RenderEntity, SyncToRenderWorld}, - texture::GpuImage, - view::{ - ColorGrading, ExtractedView, ExtractedWindows, Msaa, NoIndirectDrawing, RenderLayers, - RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset, Visibility, VisibleEntities, - }, - Extract, -}; -use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{ - change_detection::DetectChanges, - component::{Component, HookContext}, - entity::{ContainsEntity, Entity}, - event::EventReader, - prelude::With, - query::Has, - reflect::ReflectComponent, - resource::Resource, - system::{Commands, Query, Res, ResMut}, - world::DeferredWorld, +use crate::primitives::Frustum; + +use super::{ + visibility::{Visibility, VisibleEntities}, + ClearColorConfig, }; +use bevy_asset::Handle; +use bevy_derive::Deref; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_image::Image; -use bevy_math::{ops, vec2, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3}; -use bevy_platform::collections::{HashMap, HashSet}; +use bevy_math::{ops, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, Vec2, Vec3}; use bevy_reflect::prelude::*; -use bevy_render_macros::ExtractComponent; use bevy_transform::components::{GlobalTransform, Transform}; -use bevy_window::{ - NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, - WindowScaleFactorChanged, -}; +use bevy_window::WindowRef; use core::ops::Range; use derive_more::derive::From; use thiserror::Error; -use tracing::warn; -use wgpu::{BlendState, TextureFormat, TextureUsages}; +use wgpu_types::{BlendState, TextureUsages}; /// Render viewport configuration for the [`Camera`] component. /// @@ -110,8 +79,32 @@ impl Viewport { } } } + + pub fn with_override( + &self, + main_pass_resolution_override: Option<&MainPassResolutionOverride>, + ) -> Self { + let mut viewport = self.clone(); + if let Some(override_size) = main_pass_resolution_override { + viewport.physical_size = **override_size; + } + viewport + } } +/// Override the resolution a 3d camera's main pass is rendered at. +/// +/// Does not affect post processing. +/// +/// ## Usage +/// +/// * Insert this component on a 3d camera entity in the render world. +/// * The resolution override must be smaller than the camera's viewport size. +/// * The resolution override is specified in physical pixels. +#[derive(Component, Reflect, Deref)] +#[reflect(Component)] +pub struct MainPassResolutionOverride(pub UVec2); + /// Settings to define a camera sub view. /// /// When [`Camera::sub_camera_view`] is `Some`, only the sub-section of the @@ -176,11 +169,11 @@ pub struct RenderTargetInfo { /// Holds internally computed [`Camera`] values. #[derive(Default, Debug, Clone)] pub struct ComputedCameraValues { - clip_from_view: Mat4, - target_info: Option, + pub clip_from_view: Mat4, + pub target_info: Option, // size of the `Viewport` - old_viewport_size: Option, - old_sub_camera_view: Option, + pub old_viewport_size: Option, + pub old_sub_camera_view: Option, } /// How much energy a `Camera3d` absorbs from incoming light. @@ -291,7 +284,7 @@ impl Default for PhysicalCameraParameters { pub enum ViewportConversionError { /// The pre-computed size of the viewport was not available. /// - /// This may be because the `Camera` was just created and [`camera_system`] has not been executed + /// This may be because the `Camera` was just created and `camera_system` has not been executed /// yet, or because the [`RenderTarget`] is misconfigured in one of the following ways: /// - it references the [`PrimaryWindow`](RenderTarget::Window) when there is none, /// - it references a [`Window`](RenderTarget::Window) entity that doesn't exist or doesn't actually have a `Window` component, @@ -310,8 +303,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`](super::projection::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, } @@ -324,7 +317,7 @@ pub enum ViewportConversionError { /// to transform the 3D objects into a 2D image, as well as the render target into which that image /// is produced. /// -/// Note that a [`Camera`] needs a [`CameraRenderGraph`] to render anything. +/// Note that a [`Camera`] needs a `CameraRenderGraph` to render anything. /// This is typically provided by adding a [`Camera2d`] or [`Camera3d`] component, /// but custom render graphs can also be defined. Inserting a [`Camera`] with no render /// graph will emit an error at runtime. @@ -333,15 +326,12 @@ pub enum ViewportConversionError { /// [`Camera3d`]: https://docs.rs/bevy/latest/bevy/core_pipeline/core_3d/struct.Camera3d.html #[derive(Component, Debug, Reflect, Clone)] #[reflect(Component, Default, Debug, Clone)] -#[component(on_add = warn_on_no_render_graph)] #[require( Frustum, CameraMainTextureUsages, VisibleEntities, Transform, - Visibility, - Msaa, - SyncToRenderWorld + Visibility )] pub struct Camera { /// If set, this camera will render to the given [`Viewport`] rectangle within the configured [`RenderTarget`]. @@ -356,9 +346,6 @@ pub struct Camera { pub computed: ComputedCameraValues, /// The "target" that this camera will render to. pub target: RenderTarget, - /// If this is set to `true`, the camera will use an intermediate "high dynamic range" render texture. - /// This allows rendering with a wider range of lighting values. - pub hdr: bool, // todo: reflect this when #6042 lands /// The [`CameraOutputMode`] for this camera. #[reflect(ignore, clone)] @@ -374,12 +361,6 @@ pub struct Camera { pub sub_camera_view: Option, } -fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { - if !world.entity(entity).contains::() { - warn!("{}Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Consider adding a `Camera2d` or `Camera3d` component, or manually adding a `CameraRenderGraph` component if you need a custom render graph.", caller.map(|location|format!("{location}: ")).unwrap_or_default()); - } -} - impl Default for Camera { fn default() -> Self { Self { @@ -389,7 +370,6 @@ impl Default for Camera { computed: Default::default(), target: Default::default(), output_mode: Default::default(), - hdr: false, msaa_writeback: true, clear_color: Default::default(), sub_camera_view: None, @@ -493,7 +473,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`](super::projection::Projection). #[inline] pub fn clip_from_view(&self) -> Mat4 { self.computed.clip_from_view @@ -604,8 +584,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)); @@ -659,7 +638,8 @@ 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`](super::projection::Projection) contain `NAN`. /// /// # Panics /// @@ -671,7 +651,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) @@ -685,15 +665,15 @@ 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`](super::projection::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); @@ -719,12 +699,13 @@ impl Camera { } } -/// Control how this camera outputs once rendering is completed. +/// Control how this [`Camera`] outputs once rendering is completed. #[derive(Debug, Clone, Copy)] pub enum CameraOutputMode { /// Writes the camera output to configured render target. Write { /// The blend state that will be used by the pipeline that writes the intermediate render textures to the final render target texture. + /// If not set, the output will be written as-is, ignoring `clear_color` and the existing data in the final render target texture. blend_state: Option, /// The clear color operation to perform on the final render target texture. clear_color: ClearColorConfig, @@ -747,27 +728,7 @@ impl Default for CameraOutputMode { } } -/// Configures the [`RenderGraph`](crate::render_graph::RenderGraph) name assigned to be run for a given [`Camera`] entity. -#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)] -#[reflect(opaque)] -#[reflect(Component, Debug, Clone)] -pub struct CameraRenderGraph(InternedRenderSubGraph); - -impl CameraRenderGraph { - /// Creates a new [`CameraRenderGraph`] from any string-like type. - #[inline] - pub fn new(name: T) -> Self { - Self(name.intern()) - } - - /// Sets the graph name. - #[inline] - pub fn set(&mut self, name: T) { - self.0 = name.intern(); - } -} - -/// The "target" that a [`Camera`] will render to. For example, this could be a [`Window`] +/// The "target" that a [`Camera`] will render to. For example, this could be a `Window` /// swapchain or an [`Image`]. #[derive(Debug, Clone, Reflect, From)] #[reflect(Clone)] @@ -781,6 +742,23 @@ pub enum RenderTarget { TextureView(ManualTextureViewHandle), } +impl RenderTarget { + /// Get a handle to the render target's image, + /// or `None` if the render target is another variant. + pub fn as_image(&self) -> Option<&Handle> { + if let Self::Image(image_target) = self { + Some(&image_target.handle) + } else { + None + } + } +} + +/// A unique id that corresponds to a specific `ManualTextureView` in the `ManualTextureViews` collection. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Component, Reflect)] +#[reflect(Component, Default, Debug, PartialEq, Hash, Clone)] +pub struct ManualTextureViewHandle(pub u32); + /// A render target that renders to an [`Image`]. #[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)] #[reflect(Clone, PartialEq, Hash)] @@ -813,257 +791,12 @@ impl Default for RenderTarget { } } -/// Normalized version of the render target. -/// -/// Once we have this we shouldn't need to resolve it down anymore. -#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord, From)] -#[reflect(Clone, PartialEq, Hash)] -pub enum NormalizedRenderTarget { - /// Window to which the camera's view is rendered. - Window(NormalizedWindowRef), - /// Image to which the camera's view is rendered. - Image(ImageRenderTarget), - /// Texture View to which the camera's view is rendered. - /// Useful when the texture view needs to be created outside of Bevy, for example OpenXR. - TextureView(ManualTextureViewHandle), -} - -impl RenderTarget { - /// Normalize the render target down to a more concrete value, mostly used for equality comparisons. - pub fn normalize(&self, primary_window: Option) -> Option { - match self { - RenderTarget::Window(window_ref) => window_ref - .normalize(primary_window) - .map(NormalizedRenderTarget::Window), - RenderTarget::Image(handle) => Some(NormalizedRenderTarget::Image(handle.clone())), - RenderTarget::TextureView(id) => Some(NormalizedRenderTarget::TextureView(*id)), - } - } - - /// Get a handle to the render target's image, - /// or `None` if the render target is another variant. - pub fn as_image(&self) -> Option<&Handle> { - if let Self::Image(image_target) = self { - Some(&image_target.handle) - } else { - None - } - } -} - -impl NormalizedRenderTarget { - pub fn get_texture_view<'a>( - &self, - windows: &'a ExtractedWindows, - images: &'a RenderAssets, - manual_texture_views: &'a ManualTextureViews, - ) -> Option<&'a TextureView> { - match self { - NormalizedRenderTarget::Window(window_ref) => windows - .get(&window_ref.entity()) - .and_then(|window| window.swap_chain_texture_view.as_ref()), - NormalizedRenderTarget::Image(image_target) => images - .get(&image_target.handle) - .map(|image| &image.texture_view), - NormalizedRenderTarget::TextureView(id) => { - manual_texture_views.get(id).map(|tex| &tex.texture_view) - } - } - } - - /// Retrieves the [`TextureFormat`] of this render target, if it exists. - pub fn get_texture_format<'a>( - &self, - windows: &'a ExtractedWindows, - images: &'a RenderAssets, - manual_texture_views: &'a ManualTextureViews, - ) -> Option { - match self { - NormalizedRenderTarget::Window(window_ref) => windows - .get(&window_ref.entity()) - .and_then(|window| window.swap_chain_texture_format), - NormalizedRenderTarget::Image(image_target) => images - .get(&image_target.handle) - .map(|image| image.texture_format), - NormalizedRenderTarget::TextureView(id) => { - manual_texture_views.get(id).map(|tex| tex.format) - } - } - } - - pub fn get_render_target_info<'a>( - &self, - resolutions: impl IntoIterator, - images: &Assets, - manual_texture_views: &ManualTextureViews, - ) -> Option { - match self { - NormalizedRenderTarget::Window(window_ref) => resolutions - .into_iter() - .find(|(entity, _)| *entity == window_ref.entity()) - .map(|(_, window)| RenderTargetInfo { - physical_size: window.physical_size(), - scale_factor: window.resolution.scale_factor(), - }), - NormalizedRenderTarget::Image(image_target) => { - let image = images.get(&image_target.handle)?; - Some(RenderTargetInfo { - physical_size: image.size(), - scale_factor: image_target.scale_factor.0, - }) - } - NormalizedRenderTarget::TextureView(id) => { - manual_texture_views.get(id).map(|tex| RenderTargetInfo { - physical_size: tex.size, - scale_factor: 1.0, - }) - } - } - } - - // Check if this render target is contained in the given changed windows or images. - fn is_changed( - &self, - changed_window_ids: &HashSet, - changed_image_handles: &HashSet<&AssetId>, - ) -> bool { - match self { - NormalizedRenderTarget::Window(window_ref) => { - changed_window_ids.contains(&window_ref.entity()) - } - NormalizedRenderTarget::Image(image_target) => { - changed_image_handles.contains(&image_target.handle.id()) - } - NormalizedRenderTarget::TextureView(_) => true, - } - } -} - -/// System in charge of updating a [`Camera`] when its window or projection changes. -/// -/// The system detects window creation, resize, and scale factor change events to update the camera -/// [`Projection`] if needed. -/// -/// ## World Resources -/// -/// [`Res>`](Assets) -- For cameras that render to an image, this resource is used to -/// inspect information about the render target. This system will not access any other image assets. -/// -/// [`OrthographicProjection`]: crate::camera::OrthographicProjection -/// [`PerspectiveProjection`]: crate::camera::PerspectiveProjection -pub fn camera_system( - mut window_resized_events: EventReader, - mut window_created_events: EventReader, - mut window_scale_factor_changed_events: EventReader, - mut image_asset_events: EventReader>, - primary_window: Query>, - windows: Query<(Entity, &Window)>, - images: Res>, - manual_texture_views: Res, - mut cameras: Query<(&mut Camera, &mut Projection)>, -) { - let primary_window = primary_window.iter().next(); - - let mut changed_window_ids = >::default(); - changed_window_ids.extend(window_created_events.read().map(|event| event.window)); - changed_window_ids.extend(window_resized_events.read().map(|event| event.window)); - let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_events - .read() - .map(|event| event.window) - .collect(); - changed_window_ids.extend(scale_factor_changed_window_ids.clone()); - - let changed_image_handles: HashSet<&AssetId> = image_asset_events - .read() - .filter_map(|event| match event { - AssetEvent::Modified { id } | AssetEvent::Added { id } => Some(id), - _ => None, - }) - .collect(); - - for (mut camera, mut camera_projection) in &mut cameras { - let mut viewport_size = camera - .viewport - .as_ref() - .map(|viewport| viewport.physical_size); - - if let Some(normalized_target) = camera.target.normalize(primary_window) { - if normalized_target.is_changed(&changed_window_ids, &changed_image_handles) - || camera.is_added() - || camera_projection.is_changed() - || camera.computed.old_viewport_size != viewport_size - || camera.computed.old_sub_camera_view != camera.sub_camera_view - { - let new_computed_target_info = normalized_target.get_render_target_info( - windows, - &images, - &manual_texture_views, - ); - // Check for the scale factor changing, and resize the viewport if needed. - // This can happen when the window is moved between monitors with different DPIs. - // Without this, the viewport will take a smaller portion of the window moved to - // a higher DPI monitor. - if normalized_target - .is_changed(&scale_factor_changed_window_ids, &HashSet::default()) - { - if let (Some(new_scale_factor), Some(old_scale_factor)) = ( - new_computed_target_info - .as_ref() - .map(|info| info.scale_factor), - camera - .computed - .target_info - .as_ref() - .map(|info| info.scale_factor), - ) { - let resize_factor = new_scale_factor / old_scale_factor; - if let Some(ref mut viewport) = camera.viewport { - let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2(); - viewport.physical_position = resize(viewport.physical_position); - viewport.physical_size = resize(viewport.physical_size); - viewport_size = Some(viewport.physical_size); - } - } - } - // This check is needed because when changing WindowMode to Fullscreen, the viewport may have invalid - // arguments due to a sudden change on the window size to a lower value. - // If the size of the window is lower, the viewport will match that lower value. - if let Some(viewport) = &mut camera.viewport { - let target_info = &new_computed_target_info; - if let Some(target) = target_info { - viewport.clamp_to_size(target.physical_size); - } - } - camera.computed.target_info = new_computed_target_info; - if let Some(size) = camera.logical_viewport_size() { - if size.x != 0.0 && size.y != 0.0 { - camera_projection.update(size.x, size.y); - camera.computed.clip_from_view = match &camera.sub_camera_view { - Some(sub_view) => { - camera_projection.get_clip_from_view_for_sub(sub_view) - } - None => camera_projection.get_clip_from_view(), - } - } - } - } - } - - if camera.computed.old_viewport_size != viewport_size { - camera.computed.old_viewport_size = viewport_size; - } - - if camera.computed.old_sub_camera_view != camera.sub_camera_view { - camera.computed.old_sub_camera_view = camera.sub_camera_view; - } - } -} - /// This component lets you control the [`TextureUsages`] field of the main texture generated for the camera -#[derive(Component, ExtractComponent, Clone, Copy, Reflect)] +#[derive(Component, Clone, Copy, Reflect)] #[reflect(opaque)] #[reflect(Component, Default, Clone)] pub struct CameraMainTextureUsages(pub TextureUsages); + impl Default for CameraMainTextureUsages { fn default() -> Self { Self( @@ -1074,271 +807,9 @@ impl Default for CameraMainTextureUsages { } } -#[derive(Component, Debug)] -pub struct ExtractedCamera { - pub target: Option, - pub physical_viewport_size: Option, - pub physical_target_size: Option, - pub viewport: Option, - pub render_graph: InternedRenderSubGraph, - pub order: isize, - pub output_mode: CameraOutputMode, - pub msaa_writeback: bool, - pub clear_color: ClearColorConfig, - pub sorted_camera_index_for_target: usize, - pub exposure: f32, - pub hdr: bool, -} - -pub fn extract_cameras( - mut commands: Commands, - query: Extract< - Query<( - Entity, - RenderEntity, - &Camera, - &CameraRenderGraph, - &GlobalTransform, - &VisibleEntities, - &Frustum, - Option<&ColorGrading>, - Option<&Exposure>, - Option<&TemporalJitter>, - Option<&RenderLayers>, - Option<&Projection>, - Has, - )>, - >, - primary_window: Extract>>, - gpu_preprocessing_support: Res, - mapper: Extract>, -) { - let primary_window = primary_window.iter().next(); - for ( - main_entity, - render_entity, - camera, - camera_render_graph, - transform, - visible_entities, - frustum, - color_grading, - exposure, - temporal_jitter, - render_layers, - projection, - no_indirect_drawing, - ) in query.iter() - { - if !camera.is_active { - commands.entity(render_entity).remove::<( - ExtractedCamera, - ExtractedView, - RenderVisibleEntities, - TemporalJitter, - RenderLayers, - Projection, - NoIndirectDrawing, - ViewUniformOffset, - )>(); - continue; - } - - let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone(); - - if let ( - Some(URect { - min: viewport_origin, - .. - }), - Some(viewport_size), - Some(target_size), - ) = ( - camera.physical_viewport_rect(), - camera.physical_viewport_size(), - camera.physical_target_size(), - ) { - if target_size.x == 0 || target_size.y == 0 { - continue; - } - - let render_visible_entities = RenderVisibleEntities { - entities: visible_entities - .entities - .iter() - .map(|(type_id, entities)| { - let entities = entities - .iter() - .map(|entity| { - let render_entity = mapper - .get(*entity) - .cloned() - .map(|entity| entity.id()) - .unwrap_or(Entity::PLACEHOLDER); - (render_entity, (*entity).into()) - }) - .collect(); - (*type_id, entities) - }) - .collect(), - }; - - let mut commands = commands.entity(render_entity); - commands.insert(( - ExtractedCamera { - target: camera.target.normalize(primary_window), - viewport: camera.viewport.clone(), - physical_viewport_size: Some(viewport_size), - physical_target_size: Some(target_size), - render_graph: camera_render_graph.0, - order: camera.order, - output_mode: camera.output_mode, - msaa_writeback: camera.msaa_writeback, - clear_color: camera.clear_color, - // this will be set in sort_cameras - sorted_camera_index_for_target: 0, - exposure: exposure - .map(Exposure::exposure) - .unwrap_or_else(|| Exposure::default().exposure()), - hdr: camera.hdr, - }, - ExtractedView { - retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0), - clip_from_view: camera.clip_from_view(), - world_from_view: *transform, - clip_from_world: None, - hdr: camera.hdr, - viewport: UVec4::new( - viewport_origin.x, - viewport_origin.y, - viewport_size.x, - viewport_size.y, - ), - color_grading, - }, - render_visible_entities, - *frustum, - )); - - if let Some(temporal_jitter) = temporal_jitter { - commands.insert(temporal_jitter.clone()); - } - - if let Some(render_layers) = render_layers { - commands.insert(render_layers.clone()); - } - - if let Some(perspective) = projection { - commands.insert(perspective.clone()); - } - - if no_indirect_drawing - || !matches!( - gpu_preprocessing_support.max_supported_mode, - GpuPreprocessingMode::Culling - ) - { - commands.insert(NoIndirectDrawing); - } - }; +impl CameraMainTextureUsages { + pub fn with(mut self, usages: TextureUsages) -> Self { + self.0 |= usages; + self } } - -/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system. -#[derive(Resource, Default)] -pub struct SortedCameras(pub Vec); - -pub struct SortedCamera { - pub entity: Entity, - pub order: isize, - pub target: Option, - pub hdr: bool, -} - -pub fn sort_cameras( - mut sorted_cameras: ResMut, - mut cameras: Query<(Entity, &mut ExtractedCamera)>, -) { - sorted_cameras.0.clear(); - for (entity, camera) in cameras.iter() { - sorted_cameras.0.push(SortedCamera { - entity, - order: camera.order, - target: camera.target.clone(), - hdr: camera.hdr, - }); - } - // sort by order and ensure within an order, RenderTargets of the same type are packed together - sorted_cameras - .0 - .sort_by(|c1, c2| (c1.order, &c1.target).cmp(&(c2.order, &c2.target))); - let mut previous_order_target = None; - let mut ambiguities = >::default(); - let mut target_counts = >::default(); - for sorted_camera in &mut sorted_cameras.0 { - let new_order_target = (sorted_camera.order, sorted_camera.target.clone()); - if let Some(previous_order_target) = previous_order_target { - if previous_order_target == new_order_target { - ambiguities.insert(new_order_target.clone()); - } - } - if let Some(target) = &sorted_camera.target { - let count = target_counts - .entry((target.clone(), sorted_camera.hdr)) - .or_insert(0usize); - let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap(); - camera.sorted_camera_index_for_target = *count; - *count += 1; - } - previous_order_target = Some(new_order_target); - } - - if !ambiguities.is_empty() { - warn!( - "Camera order ambiguities detected for active cameras with the following priorities: {:?}. \ - To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \ - Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \ - result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \ - ambiguities could result in unpredictable render results.", - ambiguities - ); - } -} - -/// A subpixel offset to jitter a perspective camera's frustum by. -/// -/// Useful for temporal rendering techniques. -/// -/// Do not use with [`OrthographicProjection`]. -/// -/// [`OrthographicProjection`]: crate::camera::OrthographicProjection -#[derive(Component, Clone, Default, Reflect)] -#[reflect(Default, Component, Clone)] -pub struct TemporalJitter { - /// Offset is in range [-0.5, 0.5]. - pub offset: Vec2, -} - -impl TemporalJitter { - pub fn jitter_projection(&self, clip_from_view: &mut Mat4, view_size: Vec2) { - if clip_from_view.w_axis.w == 1.0 { - warn!( - "TemporalJitter not supported with OrthographicProjection. Use PerspectiveProjection instead." - ); - return; - } - - // https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/docs/techniques/media/super-resolution-temporal/jitter-space.svg - let jitter = (self.offset * vec2(2.0, -2.0)) / view_size; - - clip_from_view.z_axis.x += jitter.x; - clip_from_view.z_axis.y += jitter.y; - } -} - -/// Camera component specifying a mip bias to apply when sampling from material textures. -/// -/// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness. -#[derive(Default, Component, Reflect)] -#[reflect(Default, Component)] -pub struct MipBias(pub f32); diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_camera/src/clear_color.rs similarity index 70% rename from crates/bevy_render/src/camera/clear_color.rs rename to crates/bevy_camera/src/clear_color.rs index 157bcf8998..aeff7b3428 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_camera/src/clear_color.rs @@ -1,4 +1,3 @@ -use crate::extract_resource::ExtractResource; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; @@ -6,7 +5,9 @@ use bevy_reflect::prelude::*; use derive_more::derive::From; use serde::{Deserialize, Serialize}; -/// For a camera, specifies the color used to clear the viewport before rendering. +/// For a camera, specifies the color used to clear the viewport +/// [before rendering](crate::camera::Camera::clear_color) +/// or when [writing to the final render target texture](crate::camera::Camera::output_mode). #[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default, From)] #[reflect(Serialize, Deserialize, Default, Clone)] pub enum ClearColorConfig { @@ -21,11 +22,16 @@ pub enum ClearColorConfig { None, } -/// A [`Resource`] that stores the color that is used to clear the screen between frames. +/// A [`Resource`] that stores the default color that cameras use to clear the screen between frames. /// /// This color appears as the "background" color for simple apps, /// when there are portions of the screen with nothing rendered. -#[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)] +/// +/// Individual cameras may use [`Camera.clear_color`] to specify a different +/// clear color or opt out of clearing their viewport. +/// +/// [`Camera.clear_color`]: crate::camera::Camera::clear_color +#[derive(Resource, Clone, Debug, Deref, DerefMut, Reflect)] #[reflect(Resource, Default, Debug, Clone)] pub struct ClearColor(pub Color); diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_camera/src/components.rs similarity index 87% rename from crates/bevy_core_pipeline/src/core_3d/camera_3d.rs rename to crates/bevy_camera/src/components.rs index 9bcb2b4f80..867936727c 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_camera/src/components.rs @@ -1,39 +1,33 @@ -use crate::{ - core_3d::graph::Core3d, - tonemapping::{DebandDither, Tonemapping}, -}; +use crate::{primitives::Frustum, Camera, CameraProjection, OrthographicProjection, Projection}; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_render::{ - camera::{Camera, CameraRenderGraph, Exposure, Projection}, - extract_component::ExtractComponent, - render_resource::{LoadOp, TextureUsages}, - view::ColorGrading, -}; +use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; +use wgpu_types::{LoadOp, TextureUsages}; + +/// A 2D camera component. Enables the 2D render graph for a [`Camera`]. +#[derive(Component, Default, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +#[require( + Camera, + Projection::Orthographic(OrthographicProjection::default_2d()), + Frustum = OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default())), +)] +pub struct Camera2d; /// A 3D camera component. Enables the main 3D render graph for a [`Camera`]. /// /// The camera coordinate space is right-handed X-right, Y-up, Z-back. /// This means "forward" is -Z. -#[derive(Component, Reflect, Clone, ExtractComponent)] -#[extract_component_filter(With)] +#[derive(Component, Reflect, Clone)] #[reflect(Component, Default, Clone)] -#[require( - Camera, - DebandDither::Enabled, - CameraRenderGraph::new(Core3d), - Projection, - Tonemapping, - ColorGrading, - Exposure -)] +#[require(Camera, Projection)] pub struct Camera3d { /// The depth clear operation to perform for the main 3d pass. pub depth_load_op: Camera3dDepthLoadOp, /// The texture usages for the depth texture created for the main 3d pass. pub depth_texture_usages: Camera3dDepthTextureUsage, - /// How many individual steps should be performed in the [`Transmissive3d`](crate::core_3d::Transmissive3d) pass. + /// How many individual steps should be performed in the `Transmissive3d` pass. /// /// Roughly corresponds to how many “layers of transparency” are rendered for screen space /// specular transmissive objects. Each step requires making one additional @@ -80,6 +74,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_camera/src/lib.rs b/crates/bevy_camera/src/lib.rs new file mode 100644 index 0000000000..bf0ededae8 --- /dev/null +++ b/crates/bevy_camera/src/lib.rs @@ -0,0 +1,37 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +mod camera; +mod clear_color; +mod components; +pub mod primitives; +mod projection; +pub mod visibility; + +pub use camera::*; +pub use clear_color::*; +pub use components::*; +pub use projection::*; + +use bevy_app::{App, Plugin}; + +#[derive(Default)] +pub struct CameraPlugin; + +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .add_plugins(( + CameraProjectionPlugin, + visibility::VisibilityPlugin, + visibility::VisibilityRangePlugin, + )); + } +} diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_camera/src/primitives.rs similarity index 86% rename from crates/bevy_render/src/primitives/mod.rs rename to crates/bevy_camera/src/primitives.rs index ca664fc338..32bb557b93 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -2,8 +2,29 @@ use core::borrow::Borrow; use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent}; use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; +use bevy_mesh::{Mesh, VertexAttributeValues}; use bevy_reflect::prelude::*; +pub trait MeshAabb { + /// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space + /// + /// Returns `None` if `self` doesn't have [`Mesh::ATTRIBUTE_POSITION`] of + /// type [`VertexAttributeValues::Float32x3`], or if `self` doesn't have any vertices. + fn compute_aabb(&self) -> Option; +} + +impl MeshAabb for Mesh { + fn compute_aabb(&self) -> Option { + let Some(VertexAttributeValues::Float32x3(values)) = + self.attribute(Mesh::ATTRIBUTE_POSITION) + else { + return None; + }; + + Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p))) + } +} + /// An axis-aligned bounding box, defined by: /// - a center, /// - the distances from the center to each faces along the axis, @@ -24,10 +45,10 @@ use bevy_reflect::prelude::*; /// It won't be updated automatically if the space occupied by the entity changes, /// for example if the vertex positions of a [`Mesh3d`] are updated. /// -/// [`Camera`]: crate::camera::Camera -/// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling -/// [`CalculateBounds`]: crate::view::visibility::VisibilitySystems::CalculateBounds -/// [`Mesh3d`]: crate::mesh::Mesh +/// [`Camera`]: crate::Camera +/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling +/// [`CalculateBounds`]: crate::visibility::VisibilitySystems::CalculateBounds +/// [`Mesh3d`]: bevy_mesh::Mesh #[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)] #[reflect(Component, Default, Debug, PartialEq, Clone)] pub struct Aabb { @@ -56,7 +77,7 @@ impl Aabb { /// /// ``` /// # use bevy_math::{Vec3, Vec3A}; - /// # use bevy_render::primitives::Aabb; + /// # use bevy_camera::primitives::Aabb; /// let bb = Aabb::enclosing([Vec3::X, Vec3::Z * 2.0, Vec3::Y * -0.5]).unwrap(); /// assert_eq!(bb.min(), Vec3A::new(0.0, -0.5, 0.0)); /// assert_eq!(bb.max(), Vec3A::new(1.0, 0.0, 2.0)); @@ -218,10 +239,10 @@ impl HalfSpace { /// It is usually updated automatically by [`update_frusta`] from the /// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity. /// -/// [`Camera`]: crate::camera::Camera -/// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling -/// [`update_frusta`]: crate::view::visibility::update_frusta -/// [`CameraProjection`]: crate::camera::CameraProjection +/// [`Camera`]: crate::Camera +/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling +/// [`update_frusta`]: crate::visibility::update_frusta +/// [`CameraProjection`]: crate::CameraProjection /// [`GlobalTransform`]: bevy_transform::components::GlobalTransform #[derive(Component, Clone, Copy, Debug, Default, Reflect)] #[reflect(Component, Default, Debug, Clone)] @@ -326,6 +347,63 @@ impl Frustum { } } +pub struct CubeMapFace { + pub target: Vec3, + pub up: Vec3, +} + +// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation +// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy. +// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection +// +// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered +// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in +// left-handed y-up cubemap coordinates. +pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [ + // +X + CubeMapFace { + target: Vec3::X, + up: Vec3::Y, + }, + // -X + CubeMapFace { + target: Vec3::NEG_X, + up: Vec3::Y, + }, + // +Y + CubeMapFace { + target: Vec3::Y, + up: Vec3::Z, + }, + // -Y + CubeMapFace { + target: Vec3::NEG_Y, + up: Vec3::NEG_Z, + }, + // +Z (with left-handed conventions, pointing forwards) + CubeMapFace { + target: Vec3::NEG_Z, + up: Vec3::Y, + }, + // -Z (with left-handed conventions, pointing backwards) + CubeMapFace { + target: Vec3::Z, + up: Vec3::Y, + }, +]; + +pub fn face_index_to_name(face_index: usize) -> &'static str { + match face_index { + 0 => "+x", + 1 => "-x", + 2 => "+y", + 3 => "-y", + 4 => "+z", + 5 => "-z", + _ => "invalid", + } +} + #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Default, Debug, Clone)] pub struct CubemapFrusta { @@ -342,6 +420,42 @@ impl CubemapFrusta { } } +/// Cubemap layout defines the order of images in a packed cubemap image. +#[derive(Default, Reflect, Debug, Clone, Copy)] +pub enum CubemapLayout { + /// layout in a vertical cross format + /// ```text + /// +y + /// -x -z +x + /// -y + /// +z + /// ``` + #[default] + CrossVertical = 0, + /// layout in a horizontal cross format + /// ```text + /// +y + /// -x -z +x +z + /// -y + /// ``` + CrossHorizontal = 1, + /// layout in a vertical sequence + /// ```text + /// +x + /// -x + /// +y + /// -y + /// -z + /// +z + /// ``` + SequenceVertical = 2, + /// layout in a horizontal sequence + /// ```text + /// +x -x +y -y -z +z + /// ``` + SequenceHorizontal = 3, +} + #[derive(Component, Debug, Default, Reflect, Clone)] #[reflect(Component, Default, Debug, Clone)] pub struct CascadesFrusta { @@ -356,7 +470,7 @@ mod tests { use bevy_math::{ops, Quat}; use bevy_transform::components::GlobalTransform; - use crate::camera::{CameraProjection, PerspectiveProjection}; + use crate::{CameraProjection, PerspectiveProjection}; use super::*; diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_camera/src/projection.rs similarity index 88% rename from crates/bevy_render/src/camera/projection.rs rename to crates/bevy_camera/src/projection.rs index a7796a1d1a..847714208d 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_camera/src/projection.rs @@ -1,9 +1,8 @@ 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 crate::{primitives::Frustum, visibility::VisibilitySystems}; +use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::prelude::*; use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; @@ -23,28 +22,16 @@ impl Plugin for CameraProjectionPlugin { .register_type::() .register_type::() .register_type::() - .add_systems( - PostStartup, - crate::camera::camera_system.in_set(CameraUpdateSystems), - ) .add_systems( PostUpdate, - ( - crate::camera::camera_system - .in_set(CameraUpdateSystems) - .before(AssetEventSystems), - crate::view::update_frusta - .in_set(VisibilitySystems::UpdateFrusta) - .after(crate::camera::camera_system) - .after(TransformSystems::Propagate), - ), + crate::visibility::update_frusta + .in_set(VisibilitySystems::UpdateFrusta) + .after(TransformSystems::Propagate), ); } } -/// Label for [`camera_system`], shared across all `T`. -/// -/// [`camera_system`]: crate::camera::camera_system +/// Label for `camera_system`, shared across all `T`. #[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)] pub struct CameraUpdateSystems; @@ -90,11 +77,10 @@ pub trait CameraProjection { /// Compute camera frustum for camera with given projection and transform. /// - /// This code is called by [`update_frusta`](crate::view::visibility::update_frusta) system + /// This code is called by [`update_frusta`](crate::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 +118,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, } @@ -162,7 +147,7 @@ impl CustomProjection { /// Returns `None` if this dynamic object is not a projection of type `P`. /// /// ``` - /// # use bevy_render::prelude::{Projection, PerspectiveProjection}; + /// # use bevy_camera::{Projection, PerspectiveProjection}; /// // For simplicity's sake, use perspective as a custom projection: /// let projection = Projection::custom(PerspectiveProjection::default()); /// let Projection::Custom(custom) = projection else { return }; @@ -185,7 +170,7 @@ impl CustomProjection { /// Returns `None` if this dynamic object is not a projection of type `P`. /// /// ``` - /// # use bevy_render::prelude::{Projection, PerspectiveProjection}; + /// # use bevy_camera::{Projection, PerspectiveProjection}; /// // For simplicity's sake, use perspective as a custom projection: /// let mut projection = Projection::custom(PerspectiveProjection::default()); /// let Projection::Custom(mut custom) = projection else { return }; @@ -205,6 +190,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 +240,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 +251,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(), } } } @@ -311,8 +290,8 @@ pub struct PerspectiveProjection { /// The aspect ratio (width divided by height) of the viewing frustum. /// - /// Bevy's [`camera_system`](crate::camera::camera_system) automatically - /// updates this value when the aspect ratio of the associated window changes. + /// Bevy's `camera_system` automatically updates this value when the aspect ratio + /// of the associated window changes. /// /// Defaults to a value of `1.0`. pub aspect_ratio: f32, @@ -430,7 +409,7 @@ impl Default for PerspectiveProjection { /// Configure the orthographic projection to two world units per window height: /// /// ``` -/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode}; +/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode}; /// let projection = Projection::Orthographic(OrthographicProjection { /// scaling_mode: ScalingMode::FixedVertical { viewport_height: 2.0 }, /// ..OrthographicProjection::default_2d() @@ -486,7 +465,7 @@ pub enum ScalingMode { /// Configure the orthographic projection to one world unit per 100 window pixels: /// /// ``` -/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode}; +/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode}; /// let projection = Projection::Orthographic(OrthographicProjection { /// scaling_mode: ScalingMode::WindowSize, /// scale: 0.01, @@ -543,7 +522,7 @@ pub struct OrthographicProjection { pub scale: f32, /// The area that the projection covers relative to `viewport_origin`. /// - /// Bevy's [`camera_system`](crate::camera::camera_system) automatically + /// Bevy's `camera_system` automatically /// updates this value when the viewport is resized depending on `OrthographicProjection`'s other fields. /// In this case, `area` should not be manually modified. /// diff --git a/crates/bevy_camera/src/visibility/mod.rs b/crates/bevy_camera/src/visibility/mod.rs new file mode 100644 index 0000000000..478db336e3 --- /dev/null +++ b/crates/bevy_camera/src/visibility/mod.rs @@ -0,0 +1,995 @@ +mod range; +mod render_layers; + +use core::any::TypeId; + +use bevy_ecs::entity::{EntityHashMap, EntityHashSet}; +use bevy_ecs::lifecycle::HookContext; +use bevy_ecs::world::DeferredWorld; +use derive_more::derive::{Deref, DerefMut}; +pub use range::*; +pub use render_layers::*; + +use bevy_app::{Plugin, PostUpdate}; +use bevy_asset::Assets; +use bevy_ecs::{hierarchy::validate_parent_has_component, prelude::*}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_transform::{components::GlobalTransform, TransformSystems}; +use bevy_utils::{Parallel, TypeIdMap}; +use smallvec::SmallVec; + +use crate::{ + camera::Camera, + primitives::{Aabb, Frustum, MeshAabb, Sphere}, + Projection, +}; +use bevy_mesh::{Mesh, Mesh2d, Mesh3d}; + +#[derive(Component, Default)] +pub struct NoCpuCulling; + +/// User indication of whether an entity is visible. Propagates down the entity hierarchy. +/// +/// If an entity is hidden in this way, all [`Children`] (and all of their children and so on) who +/// are set to [`Inherited`](Self::Inherited) will also be hidden. +/// +/// This is done by the `visibility_propagate_system` which uses the entity hierarchy and +/// `Visibility` to set the values of each entity's [`InheritedVisibility`] component. +#[derive(Component, Clone, Copy, Reflect, Debug, PartialEq, Eq, Default)] +#[reflect(Component, Default, Debug, PartialEq, Clone)] +#[require(InheritedVisibility, ViewVisibility)] +pub enum Visibility { + /// An entity with `Visibility::Inherited` will inherit the Visibility of its [`ChildOf`] target. + /// + /// A root-level entity that is set to `Inherited` will be visible. + #[default] + Inherited, + /// An entity with `Visibility::Hidden` will be unconditionally hidden. + Hidden, + /// An entity with `Visibility::Visible` will be unconditionally visible. + /// + /// Note that an entity with `Visibility::Visible` will be visible regardless of whether the + /// [`ChildOf`] target entity is hidden. + Visible, +} + +impl Visibility { + /// Toggles between `Visibility::Inherited` and `Visibility::Visible`. + /// If the value is `Visibility::Hidden`, it remains unaffected. + #[inline] + pub fn toggle_inherited_visible(&mut self) { + *self = match *self { + Visibility::Inherited => Visibility::Visible, + Visibility::Visible => Visibility::Inherited, + _ => *self, + }; + } + /// Toggles between `Visibility::Inherited` and `Visibility::Hidden`. + /// If the value is `Visibility::Visible`, it remains unaffected. + #[inline] + pub fn toggle_inherited_hidden(&mut self) { + *self = match *self { + Visibility::Inherited => Visibility::Hidden, + Visibility::Hidden => Visibility::Inherited, + _ => *self, + }; + } + /// Toggles between `Visibility::Visible` and `Visibility::Hidden`. + /// If the value is `Visibility::Inherited`, it remains unaffected. + #[inline] + pub fn toggle_visible_hidden(&mut self) { + *self = match *self { + Visibility::Visible => Visibility::Hidden, + Visibility::Hidden => Visibility::Visible, + _ => *self, + }; + } +} + +// Allows `&Visibility == Visibility` +impl PartialEq for &Visibility { + #[inline] + fn eq(&self, other: &Visibility) -> bool { + // Use the base Visibility == Visibility implementation. + >::eq(*self, other) + } +} + +// Allows `Visibility == &Visibility` +impl PartialEq<&Visibility> for Visibility { + #[inline] + fn eq(&self, other: &&Visibility) -> bool { + // Use the base Visibility == Visibility implementation. + >::eq(self, *other) + } +} + +/// Whether or not an entity is visible in the hierarchy. +/// This will not be accurate until [`VisibilityPropagate`] runs in the [`PostUpdate`] schedule. +/// +/// If this is false, then [`ViewVisibility`] should also be false. +/// +/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate +#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)] +#[reflect(Component, Default, Debug, PartialEq, Clone)] +#[component(on_insert = validate_parent_has_component::)] +pub struct InheritedVisibility(bool); + +impl InheritedVisibility { + /// An entity that is invisible in the hierarchy. + pub const HIDDEN: Self = Self(false); + /// An entity that is visible in the hierarchy. + pub const VISIBLE: Self = Self(true); + + /// Returns `true` if the entity is visible in the hierarchy. + /// Otherwise, returns `false`. + #[inline] + pub fn get(self) -> bool { + self.0 + } +} + +/// A bucket into which we group entities for the purposes of visibility. +/// +/// Bevy's various rendering subsystems (3D, 2D, etc.) want to be able to +/// quickly winnow the set of entities to only those that the subsystem is +/// tasked with rendering, to avoid spending time examining irrelevant entities. +/// At the same time, Bevy wants the [`check_visibility`] system to determine +/// all entities' visibilities at the same time, regardless of what rendering +/// subsystem is responsible for drawing them. Additionally, your application +/// may want to add more types of renderable objects that Bevy determines +/// visibility for just as it does for Bevy's built-in objects. +/// +/// The solution to this problem is *visibility classes*. A visibility class is +/// a type, typically the type of a component, that represents the subsystem +/// that renders it: for example, `Mesh3d`, `Mesh2d`, and `Sprite`. The +/// [`VisibilityClass`] component stores the visibility class or classes that +/// the entity belongs to. (Generally, an object will belong to only one +/// visibility class, but in rare cases it may belong to multiple.) +/// +/// When adding a new renderable component, you'll typically want to write an +/// add-component hook that adds the type ID of that component to the +/// [`VisibilityClass`] array. See `custom_phase_item` for an example. +// +// Note: This can't be a `ComponentId` because the visibility classes are copied +// into the render world, and component IDs are per-world. +#[derive(Clone, Component, Default, Reflect, Deref, DerefMut)] +#[reflect(Component, Default, Clone)] +pub struct VisibilityClass(pub SmallVec<[TypeId; 1]>); + +/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering. +/// +/// Each frame, this will be reset to `false` during [`VisibilityPropagate`] systems in [`PostUpdate`]. +/// Later in the frame, systems in [`CheckVisibility`] will mark any visible entities using [`ViewVisibility::set`]. +/// Because of this, values of this type will be marked as changed every frame, even when they do not change. +/// +/// If you wish to add custom visibility system that sets this value, make sure you add it to the [`CheckVisibility`] set. +/// +/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate +/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility +#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)] +#[reflect(Component, Default, Debug, PartialEq, Clone)] +pub struct ViewVisibility(bool); + +impl ViewVisibility { + /// An entity that cannot be seen from any views. + pub const HIDDEN: Self = Self(false); + + /// Returns `true` if the entity is visible in any view. + /// Otherwise, returns `false`. + #[inline] + pub fn get(self) -> bool { + self.0 + } + + /// Sets the visibility to `true`. This should not be considered reversible for a given frame, + /// as this component tracks whether or not the entity visible in _any_ view. + /// + /// This will be automatically reset to `false` every frame in [`VisibilityPropagate`] and then set + /// to the proper value in [`CheckVisibility`]. + /// + /// You should only manually set this if you are defining a custom visibility system, + /// in which case the system should be placed in the [`CheckVisibility`] set. + /// For normal user-defined entity visibility, see [`Visibility`]. + /// + /// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate + /// [`CheckVisibility`]: VisibilitySystems::CheckVisibility + #[inline] + pub fn set(&mut self) { + self.0 = true; + } +} + +/// Use this component to opt-out of built-in frustum culling for entities, see +/// [`Frustum`]. +/// +/// It can be used for example: +/// - when a [`Mesh`] is updated but its [`Aabb`] is not, which might happen with animations, +/// - when using some light effects, like wanting a [`Mesh`] out of the [`Frustum`] +/// to appear in the reflection of a [`Mesh`] within. +#[derive(Debug, Component, Default, Reflect)] +#[reflect(Component, Default, Debug)] +pub struct NoFrustumCulling; + +/// Collection of entities visible from the current view. +/// +/// This component contains all entities which are visible from the currently +/// rendered view. The collection is updated automatically by the [`VisibilitySystems::CheckVisibility`] +/// system set. Renderers can use the equivalent `RenderVisibleEntities` to optimize rendering of +/// a particular view, to prevent drawing items not visible from that view. +/// +/// This component is intended to be attached to the same entity as the [`Camera`] and +/// the [`Frustum`] defining the view. +#[derive(Clone, Component, Default, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct VisibleEntities { + #[reflect(ignore, clone)] + pub entities: TypeIdMap>, +} + +impl VisibleEntities { + pub fn get(&self, type_id: TypeId) -> &[Entity] { + match self.entities.get(&type_id) { + Some(entities) => &entities[..], + None => &[], + } + } + + pub fn get_mut(&mut self, type_id: TypeId) -> &mut Vec { + self.entities.entry(type_id).or_default() + } + + pub fn iter(&self, type_id: TypeId) -> impl DoubleEndedIterator { + self.get(type_id).iter() + } + + pub fn len(&self, type_id: TypeId) -> usize { + self.get(type_id).len() + } + + pub fn is_empty(&self, type_id: TypeId) -> bool { + self.get(type_id).is_empty() + } + + pub fn clear(&mut self, type_id: TypeId) { + self.get_mut(type_id).clear(); + } + + pub fn clear_all(&mut self) { + // Don't just nuke the hash table; we want to reuse allocations. + for entities in self.entities.values_mut() { + entities.clear(); + } + } + + pub fn push(&mut self, entity: Entity, type_id: TypeId) { + self.get_mut(type_id).push(entity); + } +} + +/// Collection of mesh entities visible for 3D lighting. +/// +/// This component contains all mesh entities visible from the current light view. +/// The collection is updated automatically by `bevy_pbr::SimulationLightSystems`. +#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] +#[reflect(Component, Debug, Default, Clone)] +pub struct VisibleMeshEntities { + #[reflect(ignore, clone)] + pub entities: Vec, +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct CubemapVisibleEntities { + #[reflect(ignore, clone)] + data: [VisibleMeshEntities; 6], +} + +impl CubemapVisibleEntities { + pub fn get(&self, i: usize) -> &VisibleMeshEntities { + &self.data[i] + } + + pub fn get_mut(&mut self, i: usize) -> &mut VisibleMeshEntities { + &mut self.data[i] + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + self.data.iter() + } + + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.data.iter_mut() + } +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct CascadesVisibleEntities { + /// Map of view entity to the visible entities for each cascade frustum. + #[reflect(ignore, clone)] + pub entities: EntityHashMap>, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub enum VisibilitySystems { + /// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems, + /// calculating and inserting an [`Aabb`] to relevant entities. + CalculateBounds, + /// Label for [`update_frusta`] in [`CameraProjectionPlugin`](crate::CameraProjectionPlugin). + UpdateFrusta, + /// Label for the system propagating the [`InheritedVisibility`] in a + /// [`ChildOf`] / [`Children`] hierarchy. + VisibilityPropagate, + /// Label for the [`check_visibility`] system updating [`ViewVisibility`] + /// of each entity and the [`VisibleEntities`] of each view.\ + /// + /// System order ambiguities between systems in this set are ignored: + /// the order of systems within this set is irrelevant, as [`check_visibility`] + /// assumes that its operations are irreversible during the frame. + CheckVisibility, + /// Label for the `mark_newly_hidden_entities_invisible` system, which sets + /// [`ViewVisibility`] to [`ViewVisibility::HIDDEN`] for entities that no + /// view has marked as visible. + MarkNewlyHiddenEntitiesInvisible, +} + +pub struct VisibilityPlugin; + +impl Plugin for VisibilityPlugin { + fn build(&self, app: &mut bevy_app::App) { + use VisibilitySystems::*; + + app.register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_required_components::() + .register_required_components::() + .register_required_components::() + .register_required_components::() + .configure_sets( + PostUpdate, + (CalculateBounds, UpdateFrusta, VisibilityPropagate) + .before(CheckVisibility) + .after(TransformSystems::Propagate), + ) + .configure_sets( + PostUpdate, + MarkNewlyHiddenEntitiesInvisible.after(CheckVisibility), + ) + .init_resource::() + .add_systems( + PostUpdate, + ( + calculate_bounds.in_set(CalculateBounds), + (visibility_propagate_system, reset_view_visibility) + .in_set(VisibilityPropagate), + check_visibility.in_set(CheckVisibility), + mark_newly_hidden_entities_invisible.in_set(MarkNewlyHiddenEntitiesInvisible), + ), + ); + app.world_mut() + .register_component_hooks::() + .on_add(add_visibility_class::); + app.world_mut() + .register_component_hooks::() + .on_add(add_visibility_class::); + } +} + +/// Computes and adds an [`Aabb`] component to entities with a +/// [`Mesh3d`] component and without a [`NoFrustumCulling`] component. +/// +/// This system is used in system set [`VisibilitySystems::CalculateBounds`]. +pub fn calculate_bounds( + mut commands: Commands, + meshes: Res>, + without_aabb: Query<(Entity, &Mesh3d), (Without, Without)>, +) { + for (entity, mesh_handle) in &without_aabb { + if let Some(mesh) = meshes.get(mesh_handle) { + if let Some(aabb) = mesh.compute_aabb() { + commands.entity(entity).try_insert(aabb); + } + } + } +} + +/// Updates [`Frustum`]. +/// +/// This system is used in [`CameraProjectionPlugin`](crate::CameraProjectionPlugin). +pub fn update_frusta( + mut views: Query< + (&GlobalTransform, &Projection, &mut Frustum), + Or<(Changed, Changed)>, + >, +) { + for (transform, projection, mut frustum) in &mut views { + *frustum = projection.compute_frustum(transform); + } +} + +fn visibility_propagate_system( + changed: Query< + (Entity, &Visibility, Option<&ChildOf>, Option<&Children>), + ( + With, + Or<(Changed, Changed)>, + ), + >, + mut visibility_query: Query<(&Visibility, &mut InheritedVisibility)>, + children_query: Query<&Children, (With, With)>, +) { + for (entity, visibility, child_of, children) in &changed { + let is_visible = match visibility { + Visibility::Visible => true, + Visibility::Hidden => false, + // fall back to true if no parent is found or parent lacks components + Visibility::Inherited => child_of + .and_then(|c| visibility_query.get(c.parent()).ok()) + .is_none_or(|(_, x)| x.get()), + }; + let (_, mut inherited_visibility) = visibility_query + .get_mut(entity) + .expect("With ensures this query will return a value"); + + // Only update the visibility if it has changed. + // This will also prevent the visibility from propagating multiple times in the same frame + // if this entity's visibility has been updated recursively by its parent. + if inherited_visibility.get() != is_visible { + inherited_visibility.0 = is_visible; + + // Recursively update the visibility of each child. + for &child in children.into_iter().flatten() { + let _ = + propagate_recursive(is_visible, child, &mut visibility_query, &children_query); + } + } + } +} + +fn propagate_recursive( + parent_is_visible: bool, + entity: Entity, + visibility_query: &mut Query<(&Visibility, &mut InheritedVisibility)>, + children_query: &Query<&Children, (With, With)>, + // BLOCKED: https://github.com/rust-lang/rust/issues/31436 + // We use a result here to use the `?` operator. Ideally we'd use a try block instead +) -> Result<(), ()> { + // Get the visibility components for the current entity. + // If the entity does not have the required components, just return early. + let (visibility, mut inherited_visibility) = visibility_query.get_mut(entity).map_err(drop)?; + + let is_visible = match visibility { + Visibility::Visible => true, + Visibility::Hidden => false, + Visibility::Inherited => parent_is_visible, + }; + + // Only update the visibility if it has changed. + if inherited_visibility.get() != is_visible { + inherited_visibility.0 = is_visible; + + // Recursively update the visibility of each child. + for &child in children_query.get(entity).ok().into_iter().flatten() { + let _ = propagate_recursive(is_visible, child, visibility_query, children_query); + } + } + + Ok(()) +} + +/// Stores all entities that were visible in the previous frame. +/// +/// As systems that check visibility judge entities visible, they remove them +/// from this set. Afterward, the `mark_newly_hidden_entities_invisible` system +/// runs and marks every mesh still remaining in this set as hidden. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct PreviousVisibleEntities(EntityHashSet); + +/// Resets the view visibility of every entity. +/// Entities that are visible will be marked as such later this frame +/// by a [`VisibilitySystems::CheckVisibility`] system. +fn reset_view_visibility( + mut query: Query<(Entity, &ViewVisibility)>, + mut previous_visible_entities: ResMut, +) { + previous_visible_entities.clear(); + + query.iter_mut().for_each(|(entity, view_visibility)| { + // Record the entities that were previously visible. + if view_visibility.get() { + previous_visible_entities.insert(entity); + } + }); +} + +/// System updating the visibility of entities each frame. +/// +/// The system is part of the [`VisibilitySystems::CheckVisibility`] set. Each +/// frame, it updates the [`ViewVisibility`] of all entities, and for each view +/// also compute the [`VisibleEntities`] for that view. +/// +/// To ensure that an entity is checked for visibility, make sure that it has a +/// [`VisibilityClass`] component and that that component is nonempty. +pub fn check_visibility( + mut thread_queues: Local>>>, + mut view_query: Query<( + Entity, + &mut VisibleEntities, + &Frustum, + Option<&RenderLayers>, + &Camera, + Has, + )>, + mut visible_aabb_query: Query<( + Entity, + &InheritedVisibility, + &mut ViewVisibility, + &VisibilityClass, + Option<&RenderLayers>, + Option<&Aabb>, + &GlobalTransform, + Has, + Has, + )>, + visible_entity_ranges: Option>, + mut previous_visible_entities: ResMut, +) { + let visible_entity_ranges = visible_entity_ranges.as_deref(); + + for (view, mut visible_entities, frustum, maybe_view_mask, camera, no_cpu_culling) in + &mut view_query + { + if !camera.is_active { + continue; + } + + let view_mask = maybe_view_mask.unwrap_or_default(); + + visible_aabb_query.par_iter_mut().for_each_init( + || thread_queues.borrow_local_mut(), + |queue, query_item| { + let ( + entity, + inherited_visibility, + mut view_visibility, + visibility_class, + maybe_entity_mask, + maybe_model_aabb, + transform, + no_frustum_culling, + has_visibility_range, + ) = query_item; + + // Skip computing visibility for entities that are configured to be hidden. + // ViewVisibility has already been reset in `reset_view_visibility`. + if !inherited_visibility.get() { + return; + } + + let entity_mask = maybe_entity_mask.unwrap_or_default(); + if !view_mask.intersects(entity_mask) { + return; + } + + // If outside of the visibility range, cull. + if has_visibility_range + && visible_entity_ranges.is_some_and(|visible_entity_ranges| { + !visible_entity_ranges.entity_is_in_range_of_view(entity, view) + }) + { + return; + } + + // If we have an aabb, do frustum culling + if !no_frustum_culling && !no_cpu_culling { + if let Some(model_aabb) = maybe_model_aabb { + let world_from_local = transform.affine(); + let model_sphere = Sphere { + center: world_from_local.transform_point3a(model_aabb.center), + radius: transform.radius_vec3a(model_aabb.half_extents), + }; + // Do quick sphere-based frustum culling + if !frustum.intersects_sphere(&model_sphere, false) { + return; + } + // Do aabb-based frustum culling + if !frustum.intersects_obb(model_aabb, &world_from_local, true, false) { + return; + } + } + } + + // Make sure we don't trigger changed notifications + // unnecessarily by checking whether the flag is set before + // setting it. + if !**view_visibility { + view_visibility.set(); + } + + // Add the entity to the queue for all visibility classes the + // entity is in. + for visibility_class_id in visibility_class.iter() { + queue.entry(*visibility_class_id).or_default().push(entity); + } + }, + ); + + visible_entities.clear_all(); + + // Drain all the thread queues into the `visible_entities` list. + for class_queues in thread_queues.iter_mut() { + for (class, entities) in class_queues { + let visible_entities_for_class = visible_entities.get_mut(*class); + for entity in entities.drain(..) { + // As we mark entities as visible, we remove them from the + // `previous_visible_entities` list. At the end, all of the + // entities remaining in `previous_visible_entities` will be + // entities that were visible last frame but are no longer + // visible this frame. + previous_visible_entities.remove(&entity); + + visible_entities_for_class.push(entity); + } + } + } + } +} + +/// Marks any entities that weren't judged visible this frame as invisible. +/// +/// As visibility-determining systems run, they remove entities that they judge +/// visible from [`PreviousVisibleEntities`]. At the end of visibility +/// determination, all entities that remain in [`PreviousVisibleEntities`] must +/// be invisible. This system goes through those entities and marks them newly +/// invisible (which sets the change flag for them). +fn mark_newly_hidden_entities_invisible( + mut view_visibilities: Query<&mut ViewVisibility>, + mut previous_visible_entities: ResMut, +) { + // Whatever previous visible entities are left are entities that were + // visible last frame but just became invisible. + for entity in previous_visible_entities.drain() { + if let Ok(mut view_visibility) = view_visibilities.get_mut(entity) { + *view_visibility = ViewVisibility::HIDDEN; + } + } +} + +/// A generic component add hook that automatically adds the appropriate +/// [`VisibilityClass`] to an entity. +/// +/// This can be handy when creating custom renderable components. To use this +/// hook, add it to your renderable component like this: +/// +/// ```ignore +/// #[derive(Component)] +/// #[component(on_add = add_visibility_class::)] +/// struct MyComponent { +/// ... +/// } +/// ``` +pub fn add_visibility_class( + mut world: DeferredWorld<'_>, + HookContext { entity, .. }: HookContext, +) where + C: 'static, +{ + if let Some(mut visibility_class) = world.get_mut::(entity) { + visibility_class.push(TypeId::of::()); + } +} + +#[cfg(test)] +mod test { + use super::*; + use bevy_app::prelude::*; + + #[test] + fn visibility_propagation() { + let mut app = App::new(); + app.add_systems(Update, visibility_propagate_system); + + let root1 = app.world_mut().spawn(Visibility::Hidden).id(); + let root1_child1 = app.world_mut().spawn(Visibility::default()).id(); + let root1_child2 = app.world_mut().spawn(Visibility::Hidden).id(); + let root1_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); + let root1_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); + + app.world_mut() + .entity_mut(root1) + .add_children(&[root1_child1, root1_child2]); + app.world_mut() + .entity_mut(root1_child1) + .add_children(&[root1_child1_grandchild1]); + app.world_mut() + .entity_mut(root1_child2) + .add_children(&[root1_child2_grandchild1]); + + let root2 = app.world_mut().spawn(Visibility::default()).id(); + let root2_child1 = app.world_mut().spawn(Visibility::default()).id(); + let root2_child2 = app.world_mut().spawn(Visibility::Hidden).id(); + let root2_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); + let root2_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); + + app.world_mut() + .entity_mut(root2) + .add_children(&[root2_child1, root2_child2]); + app.world_mut() + .entity_mut(root2_child1) + .add_children(&[root2_child1_grandchild1]); + app.world_mut() + .entity_mut(root2_child2) + .add_children(&[root2_child2_grandchild1]); + + app.update(); + + let is_visible = |e: Entity| { + app.world() + .entity(e) + .get::() + .unwrap() + .get() + }; + assert!( + !is_visible(root1), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child1), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child2), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child1_grandchild1), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child2_grandchild1), + "invisibility propagates down tree from root" + ); + + assert!( + is_visible(root2), + "visibility propagates down tree from root" + ); + assert!( + is_visible(root2_child1), + "visibility propagates down tree from root" + ); + assert!( + !is_visible(root2_child2), + "visibility propagates down tree from root, but local invisibility is preserved" + ); + assert!( + is_visible(root2_child1_grandchild1), + "visibility propagates down tree from root" + ); + assert!( + !is_visible(root2_child2_grandchild1), + "child's invisibility propagates down to grandchild" + ); + } + + #[test] + fn test_visibility_propagation_on_parent_change() { + // Setup the world and schedule + let mut app = App::new(); + + app.add_systems(Update, visibility_propagate_system); + + // Create entities with visibility and hierarchy + let parent1 = app.world_mut().spawn((Visibility::Hidden,)).id(); + let parent2 = app.world_mut().spawn((Visibility::Visible,)).id(); + let child1 = app.world_mut().spawn((Visibility::Inherited,)).id(); + let child2 = app.world_mut().spawn((Visibility::Inherited,)).id(); + + // Build hierarchy + app.world_mut() + .entity_mut(parent1) + .add_children(&[child1, child2]); + + // Run the system initially to set up visibility + app.update(); + + // Change parent visibility to Hidden + app.world_mut() + .entity_mut(parent2) + .insert(Visibility::Visible); + // Simulate a change in the parent component + app.world_mut().entity_mut(child2).insert(ChildOf(parent2)); // example of changing parent + + // Run the system again to propagate changes + app.update(); + + let is_visible = |e: Entity| { + app.world() + .entity(e) + .get::() + .unwrap() + .get() + }; + + // Retrieve and assert visibility + + assert!( + !is_visible(child1), + "Child1 should inherit visibility from parent" + ); + + assert!( + is_visible(child2), + "Child2 should inherit visibility from parent" + ); + } + + #[test] + fn visibility_propagation_unconditional_visible() { + use Visibility::{Hidden, Inherited, Visible}; + + let mut app = App::new(); + app.add_systems(Update, visibility_propagate_system); + + let root1 = app.world_mut().spawn(Visible).id(); + let root1_child1 = app.world_mut().spawn(Inherited).id(); + let root1_child2 = app.world_mut().spawn(Hidden).id(); + let root1_child1_grandchild1 = app.world_mut().spawn(Visible).id(); + let root1_child2_grandchild1 = app.world_mut().spawn(Visible).id(); + + let root2 = app.world_mut().spawn(Inherited).id(); + let root3 = app.world_mut().spawn(Hidden).id(); + + app.world_mut() + .entity_mut(root1) + .add_children(&[root1_child1, root1_child2]); + app.world_mut() + .entity_mut(root1_child1) + .add_children(&[root1_child1_grandchild1]); + app.world_mut() + .entity_mut(root1_child2) + .add_children(&[root1_child2_grandchild1]); + + app.update(); + + let is_visible = |e: Entity| { + app.world() + .entity(e) + .get::() + .unwrap() + .get() + }; + assert!( + is_visible(root1), + "an unconditionally visible root is visible" + ); + assert!( + is_visible(root1_child1), + "an inheriting child of an unconditionally visible parent is visible" + ); + assert!( + !is_visible(root1_child2), + "a hidden child on an unconditionally visible parent is hidden" + ); + assert!( + is_visible(root1_child1_grandchild1), + "an unconditionally visible child of an inheriting parent is visible" + ); + assert!( + is_visible(root1_child2_grandchild1), + "an unconditionally visible child of a hidden parent is visible" + ); + assert!(is_visible(root2), "an inheriting root is visible"); + assert!(!is_visible(root3), "a hidden root is hidden"); + } + + #[test] + fn visibility_propagation_change_detection() { + let mut world = World::new(); + let mut schedule = Schedule::default(); + schedule.add_systems(visibility_propagate_system); + + // Set up an entity hierarchy. + + let id1 = world.spawn(Visibility::default()).id(); + + let id2 = world.spawn(Visibility::default()).id(); + world.entity_mut(id1).add_children(&[id2]); + + let id3 = world.spawn(Visibility::Hidden).id(); + world.entity_mut(id2).add_children(&[id3]); + + let id4 = world.spawn(Visibility::default()).id(); + world.entity_mut(id3).add_children(&[id4]); + + // Test the hierarchy. + + // Make sure the hierarchy is up-to-date. + schedule.run(&mut world); + world.clear_trackers(); + + let mut q = world.query::>(); + + assert!(!q.get(&world, id1).unwrap().is_changed()); + assert!(!q.get(&world, id2).unwrap().is_changed()); + assert!(!q.get(&world, id3).unwrap().is_changed()); + assert!(!q.get(&world, id4).unwrap().is_changed()); + + world.clear_trackers(); + world.entity_mut(id1).insert(Visibility::Hidden); + schedule.run(&mut world); + + assert!(q.get(&world, id1).unwrap().is_changed()); + assert!(q.get(&world, id2).unwrap().is_changed()); + assert!(!q.get(&world, id3).unwrap().is_changed()); + assert!(!q.get(&world, id4).unwrap().is_changed()); + + world.clear_trackers(); + schedule.run(&mut world); + + assert!(!q.get(&world, id1).unwrap().is_changed()); + assert!(!q.get(&world, id2).unwrap().is_changed()); + assert!(!q.get(&world, id3).unwrap().is_changed()); + assert!(!q.get(&world, id4).unwrap().is_changed()); + + world.clear_trackers(); + world.entity_mut(id3).insert(Visibility::Inherited); + schedule.run(&mut world); + + assert!(!q.get(&world, id1).unwrap().is_changed()); + assert!(!q.get(&world, id2).unwrap().is_changed()); + assert!(!q.get(&world, id3).unwrap().is_changed()); + assert!(!q.get(&world, id4).unwrap().is_changed()); + + world.clear_trackers(); + world.entity_mut(id2).insert(Visibility::Visible); + schedule.run(&mut world); + + assert!(!q.get(&world, id1).unwrap().is_changed()); + assert!(q.get(&world, id2).unwrap().is_changed()); + assert!(q.get(&world, id3).unwrap().is_changed()); + assert!(q.get(&world, id4).unwrap().is_changed()); + + world.clear_trackers(); + schedule.run(&mut world); + + assert!(!q.get(&world, id1).unwrap().is_changed()); + assert!(!q.get(&world, id2).unwrap().is_changed()); + assert!(!q.get(&world, id3).unwrap().is_changed()); + assert!(!q.get(&world, id4).unwrap().is_changed()); + } + + #[test] + fn visibility_propagation_with_invalid_parent() { + let mut world = World::new(); + let mut schedule = Schedule::default(); + schedule.add_systems(visibility_propagate_system); + + let parent = world.spawn(()).id(); + let child = world.spawn(Visibility::default()).id(); + world.entity_mut(parent).add_children(&[child]); + + schedule.run(&mut world); + world.clear_trackers(); + + let child_visible = world.entity(child).get::().unwrap().0; + // defaults to same behavior of parent not found: visible = true + assert!(child_visible); + } + + #[test] + fn ensure_visibility_enum_size() { + assert_eq!(1, size_of::()); + assert_eq!(1, size_of::>()); + } +} diff --git a/crates/bevy_camera/src/visibility/range.rs b/crates/bevy_camera/src/visibility/range.rs new file mode 100644 index 0000000000..b85631c9d5 --- /dev/null +++ b/crates/bevy_camera/src/visibility/range.rs @@ -0,0 +1,295 @@ +//! Specific distances from the camera in which entities are visible, also known +//! as *hierarchical levels of detail* or *HLOD*s. + +use core::{ + hash::{Hash, Hasher}, + ops::Range, +}; + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_ecs::{ + component::Component, + entity::{Entity, EntityHashMap}, + query::With, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{Local, Query, ResMut}, +}; +use bevy_math::FloatOrd; +use bevy_reflect::Reflect; +use bevy_transform::components::GlobalTransform; +use bevy_utils::Parallel; + +use super::{check_visibility, VisibilitySystems}; +use crate::{camera::Camera, primitives::Aabb}; + +/// A plugin that enables [`VisibilityRange`]s, which allow entities to be +/// hidden or shown based on distance to the camera. +pub struct VisibilityRangePlugin; + +impl Plugin for VisibilityRangePlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .init_resource::() + .add_systems( + PostUpdate, + check_visibility_ranges + .in_set(VisibilitySystems::CheckVisibility) + .before(check_visibility), + ); + } +} + +/// Specifies the range of distances that this entity must be from the camera in +/// order to be rendered. +/// +/// This is also known as *hierarchical level of detail* or *HLOD*. +/// +/// Use this component when you want to render a high-polygon mesh when the +/// camera is close and a lower-polygon mesh when the camera is far away. This +/// is a common technique for improving performance, because fine details are +/// hard to see in a mesh at a distance. To avoid an artifact known as *popping* +/// between levels, each level has a *margin*, within which the object +/// transitions gradually from invisible to visible using a dithering effect. +/// +/// You can also use this feature to replace multiple meshes with a single mesh +/// when the camera is distant. This is the reason for the term "*hierarchical* +/// level of detail". Reducing the number of meshes can be useful for reducing +/// drawcall count. Note that you must place the [`VisibilityRange`] component +/// on each entity you want to be part of a LOD group, as [`VisibilityRange`] +/// isn't automatically propagated down to children. +/// +/// A typical use of this feature might look like this: +/// +/// | Entity | `start_margin` | `end_margin` | +/// |-------------------------|----------------|--------------| +/// | Root | N/A | N/A | +/// | ├─ High-poly mesh | [0, 0) | [20, 25) | +/// | ├─ Low-poly mesh | [20, 25) | [70, 75) | +/// | └─ Billboard *imposter* | [70, 75) | [150, 160) | +/// +/// With this setup, the user will see a high-poly mesh when the camera is +/// closer than 20 units. As the camera zooms out, between 20 units to 25 units, +/// the high-poly mesh will gradually fade to a low-poly mesh. When the camera +/// is 70 to 75 units away, the low-poly mesh will fade to a single textured +/// quad. And between 150 and 160 units, the object fades away entirely. Note +/// that the `end_margin` of a higher LOD is always identical to the +/// `start_margin` of the next lower LOD; this is important for the crossfade +/// effect to function properly. +#[derive(Component, Clone, PartialEq, Default, Reflect)] +#[reflect(Component, PartialEq, Hash, Clone)] +pub struct VisibilityRange { + /// The range of distances, in world units, between which this entity will + /// smoothly fade into view as the camera zooms out. + /// + /// If the start and end of this range are identical, the transition will be + /// abrupt, with no crossfading. + /// + /// `start_margin.end` must be less than or equal to `end_margin.start`. + pub start_margin: Range, + + /// The range of distances, in world units, between which this entity will + /// smoothly fade out of view as the camera zooms out. + /// + /// If the start and end of this range are identical, the transition will be + /// abrupt, with no crossfading. + /// + /// `end_margin.start` must be greater than or equal to `start_margin.end`. + pub end_margin: Range, + + /// If set to true, Bevy will use the center of the axis-aligned bounding + /// box ([`Aabb`]) as the position of the mesh for the purposes of + /// visibility range computation. + /// + /// Otherwise, if this field is set to false, Bevy will use the origin of + /// the mesh as the mesh's position. + /// + /// Usually you will want to leave this set to false, because different LODs + /// may have different AABBs, and smooth crossfades between LOD levels + /// require that all LODs of a mesh be at *precisely* the same position. If + /// you aren't using crossfading, however, and your meshes aren't centered + /// around their origins, then this flag may be useful. + pub use_aabb: bool, +} + +impl Eq for VisibilityRange {} + +impl Hash for VisibilityRange { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + FloatOrd(self.start_margin.start).hash(state); + FloatOrd(self.start_margin.end).hash(state); + FloatOrd(self.end_margin.start).hash(state); + FloatOrd(self.end_margin.end).hash(state); + } +} + +impl VisibilityRange { + /// Creates a new *abrupt* visibility range, with no crossfade. + /// + /// There will be no crossfade; the object will immediately vanish if the + /// camera is closer than `start` units or farther than `end` units from the + /// model. + /// + /// The `start` value must be less than or equal to the `end` value. + #[inline] + pub fn abrupt(start: f32, end: f32) -> Self { + Self { + start_margin: start..start, + end_margin: end..end, + use_aabb: false, + } + } + + /// Returns true if both the start and end transitions for this range are + /// abrupt: that is, there is no crossfading. + #[inline] + pub fn is_abrupt(&self) -> bool { + self.start_margin.start == self.start_margin.end + && self.end_margin.start == self.end_margin.end + } + + /// Returns true if the object will be visible at all, given a camera + /// `camera_distance` units away. + /// + /// Any amount of visibility, even with the heaviest dithering applied, is + /// considered visible according to this check. + #[inline] + pub fn is_visible_at_all(&self, camera_distance: f32) -> bool { + camera_distance >= self.start_margin.start && camera_distance < self.end_margin.end + } + + /// Returns true if the object is completely invisible, given a camera + /// `camera_distance` units away. + /// + /// This is equivalent to `!VisibilityRange::is_visible_at_all()`. + #[inline] + pub fn is_culled(&self, camera_distance: f32) -> bool { + !self.is_visible_at_all(camera_distance) + } +} + +/// Stores which entities are in within the [`VisibilityRange`]s of views. +/// +/// This doesn't store the results of frustum or occlusion culling; use +/// [`super::ViewVisibility`] for that. Thus entities in this list may not +/// actually be visible. +/// +/// For efficiency, these tables only store entities that have +/// [`VisibilityRange`] components. Entities without such a component won't be +/// in these tables at all. +/// +/// The table is indexed by entity and stores a 32-bit bitmask with one bit for +/// each camera, where a 0 bit corresponds to "out of range" and a 1 bit +/// corresponds to "in range". Hence it's limited to storing information for 32 +/// views. +#[derive(Resource, Default)] +pub struct VisibleEntityRanges { + /// Stores which bit index each view corresponds to. + views: EntityHashMap, + + /// Stores a bitmask in which each view has a single bit. + /// + /// A 0 bit for a view corresponds to "out of range"; a 1 bit corresponds to + /// "in range". + entities: EntityHashMap, +} + +impl VisibleEntityRanges { + /// Clears out the [`VisibleEntityRanges`] in preparation for a new frame. + fn clear(&mut self) { + self.views.clear(); + self.entities.clear(); + } + + /// Returns true if the entity is in range of the given camera. + /// + /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or + /// occlusion culling. Thus the entity might not *actually* be visible. + /// + /// The entity is assumed to have a [`VisibilityRange`] component. If the + /// entity doesn't have that component, this method will return false. + #[inline] + pub fn entity_is_in_range_of_view(&self, entity: Entity, view: Entity) -> bool { + let Some(visibility_bitmask) = self.entities.get(&entity) else { + return false; + }; + let Some(view_index) = self.views.get(&view) else { + return false; + }; + (visibility_bitmask & (1 << view_index)) != 0 + } + + /// Returns true if the entity is in range of any view. + /// + /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or + /// occlusion culling. Thus the entity might not *actually* be visible. + /// + /// The entity is assumed to have a [`VisibilityRange`] component. If the + /// entity doesn't have that component, this method will return false. + #[inline] + pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool { + self.entities.contains_key(&entity) + } +} + +/// Checks all entities against all views in order to determine which entities +/// with [`VisibilityRange`]s are potentially visible. +/// +/// This only checks distance from the camera and doesn't frustum or occlusion +/// cull. +pub fn check_visibility_ranges( + mut visible_entity_ranges: ResMut, + view_query: Query<(Entity, &GlobalTransform), With>, + mut par_local: Local>>, + entity_query: Query<(Entity, &GlobalTransform, Option<&Aabb>, &VisibilityRange)>, +) { + visible_entity_ranges.clear(); + + // Early out if the visibility range feature isn't in use. + if entity_query.is_empty() { + return; + } + + // Assign an index to each view. + let mut views = vec![]; + for (view, view_transform) in view_query.iter().take(32) { + let view_index = views.len() as u8; + visible_entity_ranges.views.insert(view, view_index); + views.push((view, view_transform.translation_vec3a())); + } + + // Check each entity/view pair. Only consider entities with + // [`VisibilityRange`] components. + entity_query.par_iter().for_each( + |(entity, entity_transform, maybe_model_aabb, visibility_range)| { + let mut visibility = 0; + for (view_index, &(_, view_position)) in views.iter().enumerate() { + // If instructed to use the AABB and the model has one, use its + // center as the model position. Otherwise, use the model's + // translation. + let model_position = match (visibility_range.use_aabb, maybe_model_aabb) { + (true, Some(model_aabb)) => entity_transform + .affine() + .transform_point3a(model_aabb.center), + _ => entity_transform.translation_vec3a(), + }; + + if visibility_range.is_visible_at_all((view_position - model_position).length()) { + visibility |= 1 << view_index; + } + } + + // Invisible entities have no entry at all in the hash map. This speeds + // up checks slightly in this common case. + if visibility != 0 { + par_local.borrow_local_mut().push((entity, visibility)); + } + }, + ); + + visible_entity_ranges.entities.extend(par_local.drain()); +} diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_camera/src/visibility/render_layers.rs similarity index 97% rename from crates/bevy_render/src/view/visibility/render_layers.rs rename to crates/bevy_camera/src/visibility/render_layers.rs index a5a58453e8..b39ecb215c 100644 --- a/crates/bevy_render/src/view/visibility/render_layers.rs +++ b/crates/bevy_camera/src/visibility/render_layers.rs @@ -7,18 +7,14 @@ pub const DEFAULT_LAYERS: &RenderLayers = &RenderLayers::layer(0); /// An identifier for a rendering layer. pub type Layer = usize; -/// Describes which rendering layers an entity belongs to. +/// Defines which rendering layers an entity belongs to. /// -/// Cameras with this component will only render entities with intersecting -/// layers. +/// A camera renders an entity only when their render layers intersect. /// -/// Entities may belong to one or more layers, or no layer at all. +/// The [`Default`] instance of `RenderLayers` contains layer `0`, the first layer. Entities +/// without this component also belong to layer `0`. /// -/// The [`Default`] instance of `RenderLayers` contains layer `0`, the first layer. -/// -/// An entity with this component without any layers is invisible. -/// -/// Entities without this component belong to layer `0`. +/// An empty `RenderLayers` makes the entity invisible. #[derive(Component, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)] #[reflect(Component, Default, PartialEq, Debug, Clone)] pub struct RenderLayers(SmallVec<[u64; INLINE_BLOCKS]>); diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index 9b6d7d8cf6..22ade12709 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -1,26 +1,26 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" 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/lib.rs b/crates/bevy_color/src/lib.rs index 712da5d7ec..d5d72d1544 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -262,6 +262,7 @@ macro_rules! impl_componentwise_vector_space { } impl bevy_math::VectorSpace for $ty { + type Scalar = f32; const ZERO: Self = Self { $($element: 0.0,)+ }; 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_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index ead2adf039..a811e8e313 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -177,8 +177,8 @@ impl Srgba { pub fn to_hex(&self) -> String { let [r, g, b, a] = self.to_u8_array(); match a { - 255 => format!("#{:02X}{:02X}{:02X}", r, g, b), - _ => format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a), + 255 => format!("#{r:02X}{g:02X}{b:02X}"), + _ => format!("#{r:02X}{g:02X}{b:02X}{a:02X}"), } } diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 304c007104..63513787f8 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "bevy_core_pipeline" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" authors = [ "Bevy Contributors ", "Carter Anderson ", ] description = "Provides a core render pipeline for Bevy Engine." -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -20,32 +20,30 @@ 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_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", 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", ] } -serde = { version = "1", features = ["derive"] } bitflags = "2.3" radsort = "0.1" nonmax = "0.5" -smallvec = "1" +smallvec = { version = "1", default-features = false } thiserror = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } -bytemuck = { version = "1" } [lints] workspace = true 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/auto_exposure/mod.rs b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs index 7e7e6c1af7..7a33df99d8 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs @@ -1,12 +1,12 @@ use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; +use bevy_asset::{embedded_asset, AssetApp, Assets, Handle}; use bevy_ecs::prelude::*; use bevy_render::{ extract_component::ExtractComponentPlugin, render_asset::RenderAssetPlugin, - render_graph::RenderGraphApp, + render_graph::RenderGraphExt, render_resource::{ - Buffer, BufferDescriptor, BufferUsages, PipelineCache, Shader, SpecializedComputePipelines, + Buffer, BufferDescriptor, BufferUsages, PipelineCache, SpecializedComputePipelines, }, renderer::RenderDevice, ExtractSchedule, Render, RenderApp, RenderSystems, @@ -21,9 +21,7 @@ mod settings; use buffers::{extract_buffers, prepare_buffers, AutoExposureBuffers}; pub use compensation_curve::{AutoExposureCompensationCurve, AutoExposureCompensationCurveError}; use node::AutoExposureNode; -use pipeline::{ - AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline, METERING_SHADER_HANDLE, -}; +use pipeline::{AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline}; pub use settings::AutoExposure; use crate::{ @@ -43,12 +41,7 @@ struct AutoExposureResources { impl Plugin for AutoExposurePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - METERING_SHADER_HANDLE, - "auto_exposure.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "auto_exposure.wgsl"); app.add_plugins(RenderAssetPlugin::::default()) .register_type::() diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs index 06fa118827..4a2afa939e 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs @@ -1,7 +1,7 @@ use super::compensation_curve::{ AutoExposureCompensationCurve, AutoExposureCompensationCurveUniform, }; -use bevy_asset::{prelude::*, weak_handle}; +use bevy_asset::{load_embedded_asset, prelude::*}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_render::{ @@ -10,6 +10,7 @@ use bevy_render::{ renderer::RenderDevice, view::ViewUniform, }; +use bevy_utils::default; use core::num::NonZero; #[derive(Resource)] @@ -44,9 +45,6 @@ pub enum AutoExposurePass { Average, } -pub const METERING_SHADER_HANDLE: Handle = - weak_handle!("05c84384-afa4-41d9-844e-e9cd5e7609af"); - pub const HISTOGRAM_BIN_COUNT: u64 = 64; impl FromWorld for AutoExposurePipeline { @@ -71,7 +69,7 @@ impl FromWorld for AutoExposurePipeline { ), ), ), - histogram_shader: METERING_SHADER_HANDLE.clone(), + histogram_shader: load_embedded_asset!(world, "auto_exposure.wgsl"), } } } @@ -85,12 +83,11 @@ impl SpecializedComputePipeline for AutoExposurePipeline { layout: vec![self.histogram_layout.clone()], shader: self.histogram_shader.clone(), shader_defs: vec![], - entry_point: match pass { + entry_point: Some(match pass { AutoExposurePass::Histogram => "compute_histogram".into(), AutoExposurePass::Average => "compute_average".into(), - }, - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + }), + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs index cf6fdd4e24..ae359a8a01 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs @@ -5,7 +5,7 @@ use bevy_asset::Handle; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_image::Image; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::extract_component::ExtractComponent; +use bevy_render::{extract_component::ExtractComponent, view::Hdr}; use bevy_utils::default; /// Component that enables auto exposure for an HDR-enabled 2d or 3d camera. @@ -25,6 +25,7 @@ use bevy_utils::default; /// **Auto Exposure requires compute shaders and is not compatible with WebGL2.** #[derive(Component, Clone, Reflect, ExtractComponent)] #[reflect(Component, Default, Clone)] +#[require(Hdr)] pub struct AutoExposure { /// The range of exposure values for the histogram. /// diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 53c54c6d2d..5acd98dd30 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -1,5 +1,6 @@ +use crate::FullscreenShader; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_ecs::prelude::*; use bevy_render::{ render_resource::{ @@ -9,17 +10,14 @@ use bevy_render::{ renderer::RenderDevice, RenderApp, }; - -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; - -pub const BLIT_SHADER_HANDLE: Handle = weak_handle!("59be3075-c34e-43e7-bf24-c8fe21a0192e"); +use bevy_utils::default; /// Adds support for specialized "blit pipelines", which can be used to write one texture to another. pub struct BlitPlugin; impl Plugin for BlitPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, BLIT_SHADER_HANDLE, "blit.wgsl", Shader::from_wgsl); + embedded_asset!(app, "blit.wgsl"); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.allow_ambiguous_resource::>(); @@ -40,6 +38,8 @@ impl Plugin for BlitPlugin { pub struct BlitPipeline { pub texture_bind_group: BindGroupLayout, pub sampler: Sampler, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, } impl FromWorld for BlitPipeline { @@ -62,6 +62,8 @@ impl FromWorld for BlitPipeline { BlitPipeline { texture_bind_group, sampler, + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "blit.wgsl"), } } } @@ -80,25 +82,21 @@ 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: BLIT_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "fs_main".into(), + shader: self.fragment_shader.clone(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: key.blend_state, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, multisample: MultisampleState { count: key.samples, - ..Default::default() + ..default() }, - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index 544b420bfd..aa8d3d37af 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -1,5 +1,7 @@ -use super::{Bloom, BLOOM_SHADER_HANDLE, BLOOM_TEXTURE_FORMAT}; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use crate::FullscreenShader; + +use super::{Bloom, BLOOM_TEXTURE_FORMAT}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, resource::Resource, @@ -14,6 +16,7 @@ use bevy_render::{ }, renderer::RenderDevice, }; +use bevy_utils::default; #[derive(Component)] pub struct BloomDownsamplingPipelineIds { @@ -26,6 +29,10 @@ pub struct BloomDownsamplingPipeline { /// Layout with a texture, a sampler, and uniforms pub bind_group_layout: BindGroupLayout, pub sampler: Sampler, + /// 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)] @@ -78,6 +85,8 @@ impl FromWorld for BloomDownsamplingPipeline { BloomDownsamplingPipeline { bind_group_layout, sampler, + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "bloom.wgsl"), } } } @@ -118,22 +127,18 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { .into(), ), layout, - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: BLOOM_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point, + entry_point: Some(entry_point), targets: vec![Some(ColorTargetState { format: BLOOM_TEXTURE_FORMAT, blend: None, write_mask: ColorWrites::ALL, })], }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index cbd87d11bd..d57af1cd01 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -2,6 +2,7 @@ mod downsampling_pipeline; mod settings; mod upsampling_pipeline; +use bevy_image::ToExtents; pub use settings::{Bloom, BloomCompositeMode, BloomPrefilter}; use crate::{ @@ -9,7 +10,7 @@ use crate::{ core_3d::graph::{Core3d, Node3d}, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::embedded_asset; use bevy_color::{Gray, LinearRgba}; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_math::{ops, UVec2}; @@ -19,7 +20,7 @@ use bevy_render::{ extract_component::{ ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::*, renderer::{RenderContext, RenderDevice}, texture::{CachedTexture, TextureCache}, @@ -36,15 +37,13 @@ use upsampling_pipeline::{ prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds, }; -const BLOOM_SHADER_HANDLE: Handle = weak_handle!("c9190ddc-573b-4472-8b21-573cab502b73"); - const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat; pub struct BloomPlugin; impl Plugin for BloomPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl); + embedded_asset!(app, "bloom.wgsl"); app.register_type::(); app.register_type::(); @@ -123,7 +122,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 { @@ -349,26 +348,22 @@ fn prepare_bloom_textures( views: Query<(Entity, &ExtractedCamera, &Bloom)>, ) { for (entity, camera, bloom) in &views { - if let Some(UVec2 { - x: width, - y: height, - }) = camera.physical_viewport_size - { + if let Some(viewport) = camera.physical_viewport_size { // How many times we can halve the resolution minus one so we don't go unnecessarily low let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1; - let mip_height_ratio = if height != 0 { - bloom.max_mip_dimension as f32 / height as f32 + let mip_height_ratio = if viewport.y != 0 { + bloom.max_mip_dimension as f32 / viewport.y as f32 } else { 0. }; let texture_descriptor = TextureDescriptor { label: Some("bloom_texture"), - size: Extent3d { - width: ((width as f32 * mip_height_ratio).round() as u32).max(1), - height: ((height as f32 * mip_height_ratio).round() as u32).max(1), - depth_or_array_layers: 1, - }, + size: (viewport.as_vec2() * mip_height_ratio) + .round() + .as_uvec2() + .max(UVec2::ONE) + .to_extents(), mip_level_count: mip_count, sample_count: 1, dimension: TextureDimension::D2, diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index f6ee8dbd1e..435ed037b5 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -1,8 +1,12 @@ use super::downsampling_pipeline::BloomUniforms; -use bevy_ecs::{prelude::Component, query::QueryItem, reflect::ReflectComponent}; +use bevy_ecs::{ + prelude::Component, + query::{QueryItem, With}, + reflect::ReflectComponent, +}; use bevy_math::{AspectRatio, URect, UVec4, Vec2, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{extract_component::ExtractComponent, prelude::Camera}; +use bevy_render::{extract_component::ExtractComponent, prelude::Camera, view::Hdr}; /// Applies a bloom effect to an HDR-enabled 2d or 3d camera. /// @@ -26,6 +30,7 @@ use bevy_render::{extract_component::ExtractComponent, prelude::Camera}; /// used in Bevy as well as a visualization of the curve's respective scattering profile. #[derive(Component, Reflect, Clone)] #[reflect(Component, Default, Clone)] +#[require(Hdr)] pub struct Bloom { /// Controls the baseline of how much the image is scattered (default: 0.15). /// @@ -219,18 +224,17 @@ pub enum BloomCompositeMode { impl ExtractComponent for Bloom { type QueryData = (&'static Self, &'static Camera); - type QueryFilter = (); + 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(), camera.physical_target_size(), camera.is_active, - camera.hdr, ) { - (Some(URect { min: origin, .. }), Some(size), Some(target_size), true, true) + (Some(URect { min: origin, .. }), Some(size), Some(target_size), true) if size.x != 0 && size.y != 0 => { let threshold = bloom.prefilter.threshold; diff --git a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs index e4c4ed4a64..4a5c4d50f9 100644 --- a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs @@ -1,8 +1,9 @@ +use crate::FullscreenShader; + use super::{ - downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_SHADER_HANDLE, - BLOOM_TEXTURE_FORMAT, + 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}, resource::Resource, @@ -17,6 +18,7 @@ use bevy_render::{ renderer::RenderDevice, view::ViewTarget, }; +use bevy_utils::default; #[derive(Component)] pub struct UpsamplingPipelineIds { @@ -27,6 +29,10 @@ pub struct UpsamplingPipelineIds { #[derive(Resource)] pub struct BloomUpsamplingPipeline { pub bind_group_layout: BindGroupLayout, + /// 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)] @@ -54,7 +60,11 @@ impl FromWorld for BloomUpsamplingPipeline { ), ); - BloomUpsamplingPipeline { bind_group_layout } + BloomUpsamplingPipeline { + bind_group_layout, + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "bloom.wgsl"), + } } } @@ -103,11 +113,10 @@ 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: BLOOM_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "upsample".into(), + shader: self.fragment_shader.clone(), + entry_point: Some("upsample".into()), targets: vec![Some(ColorTargetState { format: texture_format, blend: Some(BlendState { @@ -120,12 +129,9 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { }), write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs deleted file mode 100644 index d46174192b..0000000000 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{ - core_2d::graph::Core2d, - tonemapping::{DebandDither, Tonemapping}, -}; -use bevy_ecs::prelude::*; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{ - camera::{Camera, CameraProjection, CameraRenderGraph, OrthographicProjection, Projection}, - extract_component::ExtractComponent, - primitives::Frustum, -}; -use bevy_transform::prelude::{GlobalTransform, Transform}; - -/// A 2D camera component. Enables the 2D render graph for a [`Camera`]. -#[derive(Component, Default, Reflect, Clone, ExtractComponent)] -#[extract_component_filter(With)] -#[reflect(Component, Default, Clone)] -#[require( - Camera, - DebandDither, - CameraRenderGraph::new(Core2d), - Projection::Orthographic(OrthographicProjection::default_2d()), - Frustum = OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default())), - Tonemapping::None, -)] -pub struct Camera2d; 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_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 725ac38ed9..f051c1164c 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -1,4 +1,3 @@ -mod camera_2d; mod main_opaque_pass_2d_node; mod main_transparent_pass_2d_node; @@ -34,32 +33,37 @@ pub mod graph { use core::ops::Range; use bevy_asset::UntypedAssetId; +pub use bevy_camera::Camera2d; +use bevy_image::ToExtents; use bevy_platform::collections::{HashMap, HashSet}; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingMode, + camera::CameraRenderGraph, render_phase::PhaseItemBatchSetKey, view::{ExtractedView, RetainedViewEntity}, }; -pub use camera_2d::*; pub use main_opaque_pass_2d_node::*; pub use main_transparent_pass_2d_node::*; -use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; +use crate::{ + tonemapping::{DebandDither, Tonemapping, TonemappingNode}, + upscaling::UpscalingNode, +}; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_math::FloatOrd; use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, - render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, + render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner}, render_phase::{ sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases, ViewSortedRenderPhases, }, render_resource::{ - BindGroupId, CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension, - TextureFormat, TextureUsages, + BindGroupId, CachedRenderPipelineId, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, }, renderer::RenderDevice, sync_world::MainEntity, @@ -77,6 +81,11 @@ pub struct Core2dPlugin; impl Plugin for Core2dPlugin { fn build(&self, app: &mut App) { app.register_type::() + .register_required_components::() + .register_required_components_with::(|| { + CameraRenderGraph::new(Core2d) + }) + .register_required_components_with::(|| Tonemapping::None) .add_plugins(ExtractComponentPlugin::::default()); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { @@ -474,16 +483,10 @@ pub fn prepare_core_2d_depth_textures( let cached_texture = textures .entry(camera.target.clone()) .or_insert_with(|| { - // The size of the depth texture - let size = Extent3d { - depth_or_array_layers: 1, - width: physical_target_size.x, - height: physical_target_size.y, - }; - let descriptor = TextureDescriptor { label: Some("view_depth_texture"), - size, + // The size of the depth texture + size: physical_target_size.to_extents(), mip_level_count: 1, sample_count: msaa.samples(), dimension: TextureDimension::D2, 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..c5ee7a798d 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 @@ -4,7 +4,7 @@ use crate::{ }; use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, @@ -31,6 +31,7 @@ impl ViewNode for MainOpaquePass3dNode { Option<&'static SkyboxPipelineId>, Option<&'static SkyboxBindGroup>, &'static ViewUniformOffset, + Option<&'static MainPassResolutionOverride>, ); fn run<'w>( @@ -45,7 +46,8 @@ impl ViewNode for MainOpaquePass3dNode { skybox_pipeline, skybox_bind_group, view_uniform_offset, - ): QueryItem<'w, Self::ViewQuery>, + resolution_override, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_phases), Some(alpha_mask_phases)) = ( @@ -90,7 +92,7 @@ impl ViewNode for MainOpaquePass3dNode { let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d"); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs index 0a2e98f0bf..393167227f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs @@ -1,11 +1,12 @@ use super::{Camera3d, ViewTransmissionTexture}; use crate::core_3d::Transmissive3d; use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_image::ToExtents; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, - render_resource::{Extent3d, RenderPassDescriptor, StoreOp}, + render_resource::{RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ExtractedView, ViewDepthTexture, ViewTarget}, }; @@ -27,13 +28,16 @@ impl ViewNode for MainTransmissivePass3dNode { &'static ViewTarget, Option<&'static ViewTransmissionTexture>, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view, camera_3d, target, transmission, depth): QueryItem, + (camera, view, camera_3d, target, transmission, depth, resolution_override): QueryItem< + Self::ViewQuery, + >, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -85,11 +89,7 @@ impl ViewNode for MainTransmissivePass3dNode { render_context.command_encoder().copy_texture_to_texture( target.main_texture().as_image_copy(), transmission.texture.as_image_copy(), - Extent3d { - width: physical_target_size.x, - height: physical_target_size.y, - depth_or_array_layers: 1, - }, + physical_target_size.to_extents(), ); let mut render_pass = @@ -111,7 +111,7 @@ impl ViewNode for MainTransmissivePass3dNode { render_context.begin_tracked_render_pass(render_pass_descriptor); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) { diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs index 36fe8417c4..0c70ec23a0 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs @@ -1,7 +1,7 @@ use crate::core_3d::Transparent3d; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, @@ -24,12 +24,13 @@ impl ViewNode for MainTransparentPass3dNode { &'static ExtractedView, &'static ViewTarget, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view, target, depth): QueryItem, + (camera, view, target, depth, resolution_override): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -69,7 +70,7 @@ impl ViewNode for MainTransparentPass3dNode { let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d"); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) { diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 0ff61db842..9fd7880869 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -1,4 +1,3 @@ -mod camera_3d; mod main_opaque_pass_3d_node; mod main_transmissive_pass_3d_node; mod main_transparent_pass_3d_node; @@ -70,14 +69,17 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true; use core::ops::Range; +pub use bevy_camera::{ + Camera3d, Camera3dDepthLoadOp, Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality, +}; use bevy_render::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::CameraRenderGraph, experimental::occlusion_culling::OcclusionCulling, mesh::allocator::SlabId, render_phase::PhaseItemBatchSetKey, view::{prepare_view_targets, NoIndirectDrawing, RetainedViewEntity}, }; -pub use camera_3d::*; pub use main_opaque_pass_3d_node::*; pub use main_transparent_pass_3d_node::*; @@ -85,22 +87,22 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::UntypedAssetId; use bevy_color::LinearRgba; use bevy_ecs::prelude::*; -use bevy_image::BevyDefault; +use bevy_image::{BevyDefault, ToExtents}; use bevy_math::FloatOrd; use bevy_platform::collections::{HashMap, HashSet}; use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, prelude::Msaa, - render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, + render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner}, render_phase::{ sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases, ViewSortedRenderPhases, }, render_resource::{ - CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, - TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, + CachedRenderPipelineId, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDescriptor, + TextureDimension, TextureFormat, TextureUsages, TextureView, }, renderer::RenderDevice, sync_world::{MainEntity, RenderEntity}, @@ -127,7 +129,7 @@ use crate::{ ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, }, skybox::SkyboxPlugin, - tonemapping::TonemappingNode, + tonemapping::{DebandDither, Tonemapping, TonemappingNode}, upscaling::UpscalingNode, }; @@ -139,6 +141,11 @@ impl Plugin for Core3dPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() + .register_required_components_with::(|| DebandDither::Enabled) + .register_required_components_with::(|| { + CameraRenderGraph::new(Core3d) + }) + .register_required_components::() .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())) .add_systems(PostUpdate, check_msaa); @@ -811,20 +818,14 @@ pub fn prepare_core_3d_depth_textures( let cached_texture = textures .entry((camera.target.clone(), msaa)) .or_insert_with(|| { - // The size of the depth texture - let size = Extent3d { - depth_or_array_layers: 1, - width: physical_target_size.x, - height: physical_target_size.y, - }; - let usage = *render_target_usage .get(&camera.target.clone()) .expect("The depth texture usage should already exist for this target"); let descriptor = TextureDescriptor { label: Some("view_depth_texture"), - size, + // The size of the depth texture + size: physical_target_size.to_extents(), mip_level_count: 1, sample_count: msaa.samples(), dimension: TextureDimension::D2, @@ -897,13 +898,6 @@ pub fn prepare_core_3d_transmission_textures( .or_insert_with(|| { let usage = TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST; - // The size of the transmission texture - let size = Extent3d { - depth_or_array_layers: 1, - width: physical_target_size.x, - height: physical_target_size.y, - }; - let format = if view.hdr { ViewTarget::TEXTURE_FORMAT_HDR } else { @@ -912,7 +906,8 @@ pub fn prepare_core_3d_transmission_textures( let descriptor = TextureDescriptor { label: Some("view_transmission_texture"), - size, + // The size of the transmission texture + size: physical_target_size.to_extents(), mip_level_count: 1, sample_count: 1, // No need for MSAA, as we'll only copy the main texture here dimension: TextureDimension::D2, @@ -1023,11 +1018,7 @@ pub fn prepare_prepass_textures( continue; }; - let size = Extent3d { - depth_or_array_layers: 1, - width: physical_target_size.x, - height: physical_target_size.y, - }; + let size = physical_target_size.to_extents(); let cached_depth_texture = depth_prepass.then(|| { depth_textures @@ -1042,7 +1033,8 @@ pub fn prepare_prepass_textures( format: CORE_3D_DEPTH_FORMAT, usage: TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT - | TextureUsages::TEXTURE_BINDING, + | TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari) view_formats: &[], }; texture_cache.get(&render_device, descriptor) @@ -1108,7 +1100,8 @@ pub fn prepare_prepass_textures( dimension: TextureDimension::D2, format: DEFERRED_PREPASS_FORMAT, usage: TextureUsages::RENDER_ATTACHMENT - | TextureUsages::TEXTURE_BINDING, + | TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari) view_formats: &[], }, ) 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 77430e0291..5f930d85fd 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -1,11 +1,11 @@ use crate::{ - fullscreen_vertex_shader::fullscreen_shader_vertex_state, prepass::{DeferredPrepass, ViewPrepassTextures}, + FullscreenShader, }; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset}; use bevy_ecs::prelude::*; -use bevy_math::UVec2; +use bevy_image::ToExtents; use bevy_render::{ camera::ExtractedCamera, render_resource::{binding_types::texture_2d, *}, @@ -15,26 +15,19 @@ use bevy_render::{ Render, RenderApp, RenderSystems, }; +use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT; use bevy_ecs::query::QueryItem; use bevy_render::{ render_graph::{NodeRunError, RenderGraphContext, ViewNode}, renderer::RenderContext, }; +use bevy_utils::default; -use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT; - -pub const COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE: Handle = - weak_handle!("70d91342-1c43-4b20-973f-aa6ce93aa617"); pub struct CopyDeferredLightingIdPlugin; impl Plugin for CopyDeferredLightingIdPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE, - "copy_deferred_lighting_id.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "copy_deferred_lighting_id.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -137,20 +130,20 @@ 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 = world .resource_mut::() .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: COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "fragment".into(), - targets: vec![], + shader, + ..default() }), - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, depth_write_enabled: true, @@ -158,9 +151,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() }); Self { @@ -182,18 +173,10 @@ fn prepare_deferred_lighting_id_textures( views: Query<(Entity, &ExtractedCamera), With>, ) { for (entity, camera) in &views { - if let Some(UVec2 { - x: width, - y: height, - }) = camera.physical_target_size - { + if let Some(physical_target_size) = camera.physical_target_size { let texture_descriptor = TextureDescriptor { label: Some("deferred_lighting_id_depth_texture_a"), - size: Extent3d { - width, - height, - depth_or_array_layers: 1, - }, + size: physical_target_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index ffac1eec6d..ab87fccee6 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -1,4 +1,5 @@ use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_render::camera::MainPassResolutionOverride; use bevy_render::experimental::occlusion_culling::OcclusionCulling; use bevy_render::render_graph::ViewNode; @@ -36,7 +37,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( @@ -66,6 +67,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { &'static ExtractedView, &'static ViewDepthTexture, &'static ViewPrepassTextures, + Option<&'static MainPassResolutionOverride>, Has, Has, ); @@ -74,10 +76,10 @@ 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; + let (.., occlusion_culling, no_indirect_drawing) = view_query; if !occlusion_culling || no_indirect_drawing { return Ok(()); } @@ -105,8 +107,9 @@ impl ViewNode for LateDeferredGBufferPrepassNode { fn run_deferred_prepass<'w>( graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem< + (camera, extracted_view, view_depth_texture, view_prepass_textures, resolution_override, _, _): QueryItem< 'w, + '_, ::ViewQuery, >, is_late: bool, @@ -219,7 +222,7 @@ fn run_deferred_prepass<'w>( }); let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 5eee57b8bb..c3b39e8181 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -15,7 +15,7 @@ //! [Depth of field]: https://en.wikipedia.org/wiki/Depth_of_field use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -34,7 +34,7 @@ use bevy_render::{ camera::{PhysicalCameraParameters, Projection}, extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, render_graph::{ - NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner, }, render_resource::{ binding_types::{ @@ -66,11 +66,9 @@ use crate::{ graph::{Core3d, Node3d}, Camera3d, DEPTH_TEXTURE_SAMPLING_SUPPORTED, }, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; -const DOF_SHADER_HANDLE: Handle = weak_handle!("c3580ddc-2cbc-4535-a02b-9a2959066b52"); - /// A plugin that adds support for the depth of field effect to Bevy. pub struct DepthOfFieldPlugin; @@ -206,7 +204,7 @@ enum DofPass { impl Plugin for DepthOfFieldPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, DOF_SHADER_HANDLE, "dof.wgsl", Shader::from_wgsl); + embedded_asset!(app, "dof.wgsl"); app.register_type::(); app.register_type::(); @@ -327,6 +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 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,11 +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(), + 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. @@ -794,23 +800,19 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { RenderPipelineDescriptor { label: Some("depth of field pipeline".into()), layout, - push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), - primitive: default(), - depth_stencil: None, - multisample: default(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: DOF_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point: match key.pass { + entry_point: Some(match key.pass { DofPass::GaussianHorizontal => "gaussian_horizontal".into(), DofPass::GaussianVertical => "gaussian_vertical".into(), DofPass::BokehPass0 => "bokeh_pass_0".into(), DofPass::BokehPass1 => "bokeh_pass_1".into(), - }, + }), targets, }), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs b/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs index 1223ed35ec..2080efdabb 100644 --- a/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs +++ b/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs @@ -12,7 +12,7 @@ use crate::core_3d::{ prepare_core_3d_depth_textures, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -30,7 +30,7 @@ use bevy_render::{ experimental::occlusion_culling::{ OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, }, - render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext}, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt}, render_resource::{ binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d}, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, @@ -46,12 +46,13 @@ use bevy_render::{ view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture}, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; use bitflags::bitflags; use tracing::debug; /// Identifies the `downsample_depth.wgsl` shader. -pub const DOWNSAMPLE_DEPTH_SHADER_HANDLE: Handle = - weak_handle!("a09a149e-5922-4fa4-9170-3c1a13065364"); +#[derive(Resource, Deref)] +pub struct DownsampleDepthShader(Handle); /// The maximum number of mip levels that we can produce. /// @@ -68,18 +69,16 @@ pub struct MipGenerationPlugin; impl Plugin for MipGenerationPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - DOWNSAMPLE_DEPTH_SHADER_HANDLE, - "downsample_depth.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "downsample_depth.wgsl"); + + let downsample_depth_shader = load_embedded_asset!(app, "downsample_depth.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app + .insert_resource(DownsampleDepthShader(downsample_depth_shader)) .init_resource::>() .add_render_graph_node::(Core3d, Node3d::EarlyDownsampleDepth) .add_render_graph_node::(Core3d, Node3d::LateDownsampleDepth) @@ -293,17 +292,21 @@ pub struct DownsampleDepthPipeline { bind_group_layout: BindGroupLayout, /// A handle that identifies the compiled shader. pipeline_id: Option, + /// The shader asset handle. + shader: Handle, } impl DownsampleDepthPipeline { - /// Creates a new [`DownsampleDepthPipeline`] from a bind group layout. + /// Creates a new [`DownsampleDepthPipeline`] from a bind group layout and the downsample + /// shader. /// /// This doesn't actually specialize the pipeline; that must be done /// afterward. - fn new(bind_group_layout: BindGroupLayout) -> DownsampleDepthPipeline { + fn new(bind_group_layout: BindGroupLayout, shader: Handle) -> DownsampleDepthPipeline { DownsampleDepthPipeline { bind_group_layout, pipeline_id: None, + shader, } } } @@ -334,6 +337,7 @@ fn create_downsample_depth_pipelines( pipeline_cache: Res, mut specialized_compute_pipelines: ResMut>, gpu_preprocessing_support: Res, + downsample_depth_shader: Res, mut has_run: Local, ) { // Only run once. @@ -367,10 +371,22 @@ fn create_downsample_depth_pipelines( // Initialize the pipelines. let mut downsample_depth_pipelines = DownsampleDepthPipelines { - first: DownsampleDepthPipeline::new(standard_bind_group_layout.clone()), - second: DownsampleDepthPipeline::new(standard_bind_group_layout.clone()), - first_multisample: DownsampleDepthPipeline::new(multisampled_bind_group_layout.clone()), - second_multisample: DownsampleDepthPipeline::new(multisampled_bind_group_layout.clone()), + first: DownsampleDepthPipeline::new( + standard_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), + second: DownsampleDepthPipeline::new( + standard_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), + first_multisample: DownsampleDepthPipeline::new( + multisampled_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), + second_multisample: DownsampleDepthPipeline::new( + multisampled_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), sampler, }; @@ -490,14 +506,14 @@ impl SpecializedComputePipeline for DownsampleDepthPipeline { stages: ShaderStages::COMPUTE, range: 0..4, }], - shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) { + entry_point: Some(if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) { "downsample_depth_second".into() } else { "downsample_depth_first".into() - }, - zero_initialize_workgroup_memory: false, + }), + ..default() } } } @@ -529,11 +545,7 @@ pub fn create_depth_pyramid_dummy_texture( render_device .create_texture(&TextureDescriptor { label: Some(texture_label), - size: Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, + size: Extent3d::default(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, 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..857412d8ce 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: Some("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 9e04614276..3526b3e8fb 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] pub mod auto_exposure; @@ -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/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index 331dd2408d..a2b44704f6 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -7,7 +7,7 @@ use crate::{ prepass::{DepthPrepass, MotionVectorPrepass}, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::embedded_asset; use bevy_ecs::{ component::Component, query::{QueryItem, With}, @@ -18,8 +18,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, - render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{Shader, ShaderType, SpecializedRenderPipelines}, + render_graph::{RenderGraphExt, ViewNodeRunner}, + render_resource::{ShaderType, SpecializedRenderPipelines}, Render, RenderApp, RenderSystems, }; @@ -126,19 +126,12 @@ pub struct MotionBlurUniform { _webgl2_padding: bevy_math::Vec2, } -pub const MOTION_BLUR_SHADER_HANDLE: Handle = - weak_handle!("d9ca74af-fa0a-4f11-b0f2-19613b618b93"); - /// Adds support for per-object motion blur to the app. See [`MotionBlur`] for details. pub struct MotionBlurPlugin; impl Plugin for MotionBlurPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - MOTION_BLUR_SHADER_HANDLE, - "motion_blur.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "motion_blur.wgsl"); + app.add_plugins(( ExtractComponentPlugin::::default(), UniformComponentPlugin::::default(), diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index 4eab4ff7a6..904d6c6c54 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -1,3 +1,5 @@ +use crate::FullscreenShader; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ component::Component, entity::Entity, @@ -15,28 +17,32 @@ use bevy_render::{ texture_depth_2d_multisampled, uniform_buffer_sized, }, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, - ColorWrites, FragmentState, MultisampleState, PipelineCache, PrimitiveState, - RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderDefVal, - ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, - TextureFormat, TextureSampleType, + ColorWrites, FragmentState, PipelineCache, RenderPipelineDescriptor, Sampler, + SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal, ShaderStages, ShaderType, + SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat, TextureSampleType, }, renderer::RenderDevice, view::{ExtractedView, Msaa, ViewTarget}, }; +use bevy_utils::default; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; - -use super::{MotionBlurUniform, MOTION_BLUR_SHADER_HANDLE}; +use super::MotionBlurUniform; #[derive(Resource)] pub struct MotionBlurPipeline { pub(crate) sampler: Sampler, pub(crate) layout: BindGroupLayout, pub(crate) layout_msaa: BindGroupLayout, + pub(crate) fullscreen_shader: FullscreenShader, + pub(crate) fragment_shader: Handle, } impl MotionBlurPipeline { - pub(crate) fn new(render_device: &RenderDevice) -> Self { + pub(crate) fn new( + render_device: &RenderDevice, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, + ) -> Self { let mb_layout = &BindGroupLayoutEntries::sequential( ShaderStages::FRAGMENT, ( @@ -82,6 +88,8 @@ impl MotionBlurPipeline { sampler, layout, layout_msaa, + fullscreen_shader, + fragment_shader, } } } @@ -89,7 +97,10 @@ impl MotionBlurPipeline { impl FromWorld for MotionBlurPipeline { fn from_world(render_world: &mut bevy_ecs::world::World) -> Self { let render_device = render_world.resource::().clone(); - MotionBlurPipeline::new(&render_device) + + 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) } } @@ -123,11 +134,10 @@ 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: MOTION_BLUR_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -137,12 +147,9 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 8dc51e4ed5..93116dc9fd 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -8,7 +8,7 @@ use bevy_color::LinearRgba; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::*, renderer::RenderContext, view::{Msaa, ViewTarget}, @@ -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/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 673bbc5a8b..52d5d2ddc4 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -1,19 +1,17 @@ //! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details. use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; -use bevy_ecs::{component::*, prelude::*}; +use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*}; use bevy_math::UVec2; use bevy_platform::collections::HashSet; use bevy_platform::time::Instant; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - camera::{Camera, ExtractedCamera}, + camera::{Camera, ExtractedCamera, ToNormalizedRenderTarget as _}, extract_component::{ExtractComponent, ExtractComponentPlugin}, - render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{ - BufferUsages, BufferVec, DynamicUniformBuffer, Shader, ShaderType, TextureUsages, - }, + load_shader_library, + render_graph::{RenderGraphExt, ViewNodeRunner}, + render_resource::{BufferUsages, BufferVec, DynamicUniformBuffer, ShaderType, TextureUsages}, renderer::{RenderDevice, RenderQueue}, view::Msaa, Render, RenderApp, RenderSystems, @@ -33,10 +31,6 @@ use crate::core_3d::{ /// Module that defines the necessary systems to resolve the OIT buffer and render it to the screen. pub mod resolve; -/// Shader handle for the shader that draws the transparent meshes to the OIT layers buffer. -pub const OIT_DRAW_SHADER_HANDLE: Handle = - weak_handle!("0cd3c764-39b8-437b-86b4-4e45635fc03d"); - /// Used to identify which camera will use OIT to render transparent meshes /// and to configure OIT. // TODO consider supporting multiple OIT techniques like WBOIT, Moment Based OIT, @@ -105,12 +99,7 @@ impl Component for OrderIndependentTransparencySettings { pub struct OrderIndependentTransparencyPlugin; impl Plugin for OrderIndependentTransparencyPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - OIT_DRAW_SHADER_HANDLE, - "oit_draw.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "oit_draw.wgsl"); app.add_plugins(( ExtractComponentPlugin::::default(), diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index 0e5102c954..650b65f494 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -1,9 +1,7 @@ -use crate::{ - fullscreen_vertex_shader::fullscreen_shader_vertex_state, - oit::OrderIndependentTransparencySettings, -}; +use super::OitBuffers; +use crate::{oit::OrderIndependentTransparencySettings, FullscreenShader}; use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer}; use bevy_derive::Deref; use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, @@ -15,21 +13,16 @@ use bevy_render::{ binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer}, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendComponent, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, DownlevelFlags, - FragmentState, MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor, - Shader, ShaderDefVal, ShaderStages, TextureFormat, + FragmentState, PipelineCache, RenderPipelineDescriptor, ShaderDefVal, ShaderStages, + TextureFormat, }, renderer::{RenderAdapter, RenderDevice}, view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms}, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; use tracing::warn; -use super::OitBuffers; - -/// Shader handle for the shader that sorts the OIT layers, blends the colors based on depth and renders them to the screen. -pub const OIT_RESOLVE_SHADER_HANDLE: Handle = - weak_handle!("562d2917-eb06-444d-9ade-41de76b0f5ae"); - /// Contains the render node used to run the resolve pass. pub mod node; @@ -40,12 +33,7 @@ pub const OIT_REQUIRED_STORAGE_BUFFERS: u32 = 2; pub struct OitResolvePlugin; impl Plugin for OitResolvePlugin { fn build(&self, app: &mut bevy_app::App) { - load_internal_asset!( - app, - OIT_RESOLVE_SHADER_HANDLE, - "oit_resolve.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "oit_resolve.wgsl"); } fn finish(&self, app: &mut bevy_app::App) { @@ -165,6 +153,8 @@ 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. mut cached_pipeline_id: Local>, @@ -184,7 +174,12 @@ pub fn queue_oit_resolve_pipeline( } } - let desc = specialize_oit_resolve_pipeline(key, &resolve_pipeline); + 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)); @@ -202,6 +197,8 @@ 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 { ViewTarget::TEXTURE_FORMAT_HDR @@ -216,8 +213,7 @@ fn specialize_oit_resolve_pipeline( resolve_pipeline.oit_depth_bind_group_layout.clone(), ], fragment: Some(FragmentState { - entry_point: "fragment".into(), - shader: OIT_RESOLVE_SHADER_HANDLE, + shader: load_embedded_asset!(asset_server, "oit_resolve.wgsl"), shader_defs: vec![ShaderDefVal::UInt( "LAYER_COUNT".into(), key.layer_count as u32, @@ -230,13 +226,10 @@ fn specialize_oit_resolve_pipeline( }), write_mask: ColorWrites::ALL, })], + ..default() }), - vertex: fullscreen_shader_vertex_state(), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + vertex: fullscreen_shader.to_vertex_state(), + ..default() } } diff --git a/crates/bevy_core_pipeline/src/oit/resolve/node.rs b/crates/bevy_core_pipeline/src/oit/resolve/node.rs index 14d42235f1..77352e5ecb 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/node.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/node.rs @@ -1,6 +1,6 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode}, render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor}, renderer::RenderContext, @@ -23,13 +23,14 @@ impl ViewNode for OitResolveNode { &'static ViewUniformOffset, &'static OitResolvePipelineId, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view_target, view_uniform, oit_resolve_pipeline_id, depth): QueryItem< + (camera, view_target, view_uniform, oit_resolve_pipeline_id, depth, resolution_override): QueryItem< Self::ViewQuery, >, world: &World, @@ -63,7 +64,7 @@ impl ViewNode for OitResolveNode { }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } render_pass.set_render_pipeline(pipeline); diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index fddac95066..ce77fc1a75 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -3,7 +3,7 @@ //! Currently, this consists only of chromatic aberration. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -20,9 +20,10 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, extract_component::{ExtractComponent, ExtractComponentPlugin}, + load_shader_library, render_asset::{RenderAssetUsages, RenderAssets}, render_graph::{ - NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner, }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, @@ -43,23 +44,9 @@ 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 built-in postprocessing shader `post_process.wgsl`. -const POST_PROCESSING_SHADER_HANDLE: Handle = - weak_handle!("5e8e627a-7531-484d-a988-9a38acb34e52"); -/// The handle to the chromatic aberration shader `chromatic_aberration.wgsl`. -const CHROMATIC_ABERRATION_SHADER_HANDLE: Handle = - weak_handle!("e598550e-71c3-4f5a-ba29-aebc3f88c7b5"); - -/// The handle to the default chromatic aberration lookup texture. -/// -/// This is just a 3x1 image consisting of one red pixel, one green pixel, and -/// one blue pixel, in that order. -const DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE: Handle = - weak_handle!("dc3e3307-40a1-49bb-be6d-e0634e8836b2"); - /// The default chromatic aberration intensity amount, in a fraction of the /// window size. const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02; @@ -74,6 +61,9 @@ const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8; static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; +#[derive(Resource)] +struct DefaultChromaticAberrationLut(Handle); + /// A plugin that implements a built-in postprocessing stack with some common /// effects. /// @@ -102,14 +92,14 @@ pub struct PostProcessingPlugin; pub struct ChromaticAberration { /// The lookup texture that determines the color gradient. /// - /// By default, this is a 3×1 texel texture consisting of one red pixel, one - /// green pixel, and one blue texel, in that order. This recreates the most - /// typical chromatic aberration pattern. However, you can change it to - /// achieve different artistic effects. + /// By default (if None), this is a 3×1 texel texture consisting of one red + /// pixel, one green pixel, and one blue texel, in that order. This + /// recreates the most typical chromatic aberration pattern. However, you + /// can change it to achieve different artistic effects. /// /// The texture is always sampled in its vertical center, so it should /// ordinarily have a height of 1 texel. - pub color_lut: Handle, + pub color_lut: Option>, /// The size of the streaks around the edges of objects, as a fraction of /// the window size. @@ -136,6 +126,10 @@ pub struct PostProcessingPipeline { source_sampler: Sampler, /// Specifies how to sample the chromatic aberration gradient. chromatic_aberration_lut_sampler: Sampler, + /// 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. @@ -188,35 +182,23 @@ pub struct PostProcessingNode; impl Plugin for PostProcessingPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - POST_PROCESSING_SHADER_HANDLE, - "post_process.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - CHROMATIC_ABERRATION_SHADER_HANDLE, - "chromatic_aberration.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "chromatic_aberration.wgsl"); + + embedded_asset!(app, "post_process.wgsl"); // Load the default chromatic aberration LUT. let mut assets = app.world_mut().resource_mut::>(); - assets.insert( - DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE.id(), - Image::new( - Extent3d { - width: 3, - height: 1, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(), - TextureFormat::Rgba8UnormSrgb, - RenderAssetUsages::RENDER_WORLD, - ), - ); + let default_lut = assets.add(Image::new( + Extent3d { + width: 3, + height: 1, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(), + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::RENDER_WORLD, + )); app.register_type::(); app.add_plugins(ExtractComponentPlugin::::default()); @@ -226,6 +208,7 @@ impl Plugin for PostProcessingPlugin { }; render_app + .insert_resource(DefaultChromaticAberrationLut(default_lut)) .init_resource::>() .init_resource::() .add_systems( @@ -269,7 +252,7 @@ impl Plugin for PostProcessingPlugin { impl Default for ChromaticAberration { fn default() -> Self { Self { - color_lut: DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE, + color_lut: None, intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY, max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES, } @@ -321,6 +304,8 @@ impl FromWorld for PostProcessingPipeline { bind_group_layout, source_sampler, chromatic_aberration_lut_sampler, + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "post_process.wgsl"), } } } @@ -332,22 +317,17 @@ 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: POST_PROCESSING_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "fragment_main".into(), + shader: self.fragment_shader.clone(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: default(), - depth_stencil: None, - multisample: default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -364,13 +344,14 @@ 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::(); let post_processing_pipeline = world.resource::(); let post_processing_uniform_buffers = world.resource::(); let gpu_image_assets = world.resource::>(); + let default_lut = world.resource::(); // We need a render pipeline to be prepared. let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else { @@ -378,8 +359,12 @@ impl ViewNode for PostProcessingNode { }; // We need the chromatic aberration LUT to be present. - let Some(chromatic_aberration_lut) = gpu_image_assets.get(&chromatic_aberration.color_lut) - else { + let Some(chromatic_aberration_lut) = gpu_image_assets.get( + chromatic_aberration + .color_lut + .as_ref() + .unwrap_or(&default_lut.0), + ) else { return Ok(()); }; @@ -497,7 +482,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..c4cc7b1d55 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -1,6 +1,6 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, experimental::occlusion_culling::OcclusionCulling, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, @@ -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") @@ -55,30 +55,37 @@ pub struct LatePrepassNode; impl ViewNode for LatePrepassNode { type ViewQuery = ( - &'static ExtractedCamera, - &'static ExtractedView, - &'static ViewDepthTexture, - &'static ViewPrepassTextures, - &'static ViewUniformOffset, - Option<&'static DeferredPrepass>, - Option<&'static RenderSkyboxPrepassPipeline>, - Option<&'static SkyboxPrepassBindGroup>, - Option<&'static PreviousViewUniformOffset>, - Has, - Has, - Has, + ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewDepthTexture, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + ), + ( + Option<&'static DeferredPrepass>, + Option<&'static RenderSkyboxPrepassPipeline>, + Option<&'static SkyboxPrepassBindGroup>, + Option<&'static PreviousViewUniformOffset>, + Option<&'static MainPassResolutionOverride>, + ), + ( + Has, + Has, + Has, + ), ); fn run<'w>( &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 // drawing. - let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query; + let (_, _, (occlusion_culling, no_indirect_drawing, _)) = query; if !occlusion_culling || no_indirect_drawing { return Ok(()); } @@ -100,19 +107,16 @@ fn run_prepass<'w>( graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, ( - camera, - extracted_view, - view_depth_texture, - view_prepass_textures, - view_uniform_offset, - deferred_prepass, - skybox_prepass_pipeline, - skybox_prepass_bind_group, - view_prev_uniform_offset, - _, - _, - has_deferred, - ): QueryItem<'w, ::ViewQuery>, + (camera, extracted_view, view_depth_texture, view_prepass_textures, view_uniform_offset), + ( + deferred_prepass, + skybox_prepass_pipeline, + skybox_prepass_bind_group, + view_prev_uniform_offset, + resolution_override, + ), + (_, _, has_deferred), + ): QueryItem<'w, '_, ::ViewQuery>, world: &'w World, label: &'static str, ) -> Result<(), NodeRunError> { @@ -183,7 +187,7 @@ fn run_prepass<'w>( let pass_span = diagnostics.pass_span(&mut render_pass, label); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index ede50d6d8f..40524cf221 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -1,5 +1,5 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, query::{QueryItem, With}, @@ -28,25 +28,19 @@ use bevy_render::{ Render, RenderApp, RenderSystems, }; use bevy_transform::components::Transform; -use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE}; +use bevy_utils::default; +use prepass::SkyboxPrepassPipeline; use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms}; -const SKYBOX_SHADER_HANDLE: Handle = weak_handle!("a66cf9cc-cab8-47f8-ac32-db82fdc4f29b"); - pub mod prepass; pub struct SkyboxPlugin; impl Plugin for SkyboxPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - SKYBOX_PREPASS_SHADER_HANDLE, - "skybox_prepass.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "skybox.wgsl"); + embedded_asset!(app, "skybox_prepass.wgsl"); app.register_type::().add_plugins(( ExtractComponentPlugin::::default(), @@ -76,9 +70,10 @@ impl Plugin for SkyboxPlugin { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + let shader = load_embedded_asset!(render_app.world(), "skybox.wgsl"); let render_device = render_app.world().resource::().clone(); render_app - .insert_resource(SkyboxPipeline::new(&render_device)) + .insert_resource(SkyboxPipeline::new(&render_device, shader)) .init_resource::(); } } @@ -119,7 +114,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()); @@ -129,7 +126,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, @@ -158,10 +155,11 @@ pub struct SkyboxUniforms { #[derive(Resource)] struct SkyboxPipeline { bind_group_layout: BindGroupLayout, + shader: Handle, } impl SkyboxPipeline { - fn new(render_device: &RenderDevice) -> Self { + fn new(render_device: &RenderDevice, shader: Handle) -> Self { Self { bind_group_layout: render_device.create_bind_group_layout( "skybox_bind_group_layout", @@ -176,6 +174,7 @@ impl SkyboxPipeline { ), ), ), + shader, } } } @@ -194,14 +193,10 @@ impl SpecializedRenderPipeline for SkyboxPipeline { RenderPipelineDescriptor { label: Some("skybox_pipeline".into()), layout: vec![self.bind_group_layout.clone()], - push_constant_ranges: Vec::new(), vertex: VertexState { - shader: SKYBOX_SHADER_HANDLE, - shader_defs: Vec::new(), - entry_point: "skybox_vertex".into(), - buffers: Vec::new(), + shader: self.shader.clone(), + ..default() }, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: key.depth_format, depth_write_enabled: false, @@ -224,9 +219,7 @@ impl SpecializedRenderPipeline for SkyboxPipeline { alpha_to_coverage_enabled: false, }, fragment: Some(FragmentState { - shader: SKYBOX_SHADER_HANDLE, - shader_defs: Vec::new(), - entry_point: "skybox_fragment".into(), + shader: self.shader.clone(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -237,8 +230,9 @@ impl SpecializedRenderPipeline for SkyboxPipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/skybox/prepass.rs b/crates/bevy_core_pipeline/src/skybox/prepass.rs index 658660bbc6..2133e133fe 100644 --- a/crates/bevy_core_pipeline/src/skybox/prepass.rs +++ b/crates/bevy_core_pipeline/src/skybox/prepass.rs @@ -1,6 +1,6 @@ //! Adds motion vector support to skyboxes. See [`SkyboxPrepassPipeline`] for details. -use bevy_asset::{weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ component::Component, entity::Entity, @@ -27,12 +27,9 @@ use crate::{ prepass_target_descriptors, MotionVectorPrepass, NormalPrepass, PreviousViewData, PreviousViewUniforms, }, - Skybox, + FullscreenShader, Skybox, }; -pub const SKYBOX_PREPASS_SHADER_HANDLE: Handle = - weak_handle!("7a292435-bfe6-4ed9-8d30-73bf7aa673b0"); - /// This pipeline writes motion vectors to the prepass for all [`Skybox`]es. /// /// This allows features like motion blur and TAA to work correctly on the skybox. Without this, for @@ -41,6 +38,8 @@ pub const SKYBOX_PREPASS_SHADER_HANDLE: Handle = #[derive(Resource)] pub struct SkyboxPrepassPipeline { bind_group_layout: BindGroupLayout, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// Used to specialize the [`SkyboxPrepassPipeline`]. @@ -75,6 +74,8 @@ impl FromWorld for SkyboxPrepassPipeline { ), ), ), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "skybox_prepass.wgsl"), } } } @@ -86,9 +87,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { RenderPipelineDescriptor { 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(), - primitive: default(), + vertex: self.fullscreen_shader.to_vertex_state(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: false, @@ -102,12 +101,11 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { alpha_to_coverage_enabled: false, }, fragment: Some(FragmentState { - shader: SKYBOX_PREPASS_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "fragment".into(), + shader: self.fragment_shader.clone(), targets: prepass_target_descriptors(key.normal_prepass, true, false), + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl b/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl index 1ef8156fe3..e4ecb4703c 100644 --- a/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl +++ b/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl @@ -5,6 +5,9 @@ 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(0) var view: View; diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index f546ef54d3..bd0004d342 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -1,6 +1,5 @@ -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Assets, Handle}; use bevy_ecs::prelude::*; use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -8,6 +7,7 @@ use bevy_render::{ camera::Camera, extract_component::{ExtractComponent, ExtractComponentPlugin}, extract_resource::{ExtractResource, ExtractResourcePlugin}, + load_shader_library, render_asset::{RenderAssetUsages, RenderAssets}, render_resource::{ binding_types::{sampler, texture_2d, texture_3d, uniform_buffer}, @@ -27,45 +27,24 @@ mod node; use bevy_utils::default; pub use node::TonemappingNode; -const TONEMAPPING_SHADER_HANDLE: Handle = - weak_handle!("e239c010-c25c-42a1-b4e8-08818764d667"); - -const TONEMAPPING_SHARED_SHADER_HANDLE: Handle = - weak_handle!("61dbc544-4b30-4ca9-83bd-4751b5cfb1b1"); - -const TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("d50e3a70-c85e-4725-a81e-72fc83281145"); +use crate::FullscreenShader; /// 3D LUT (look up table) textures used for tonemapping #[derive(Resource, Clone, ExtractResource)] pub struct TonemappingLuts { - blender_filmic: Handle, - agx: Handle, - tony_mc_mapface: Handle, + pub blender_filmic: Handle, + pub agx: Handle, + pub tony_mc_mapface: Handle, } pub struct TonemappingPlugin; impl Plugin for TonemappingPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - TONEMAPPING_SHADER_HANDLE, - "tonemapping.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - TONEMAPPING_SHARED_SHADER_HANDLE, - "tonemapping_shared.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE, - "lut_bindings.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "tonemapping_shared.wgsl"); + load_shader_library!(app, "lut_bindings.wgsl"); + + embedded_asset!(app, "tonemapping.wgsl"); if !app.world().is_resource_added::() { let mut images = app.world_mut().resource_mut::>(); @@ -134,6 +113,8 @@ impl Plugin for TonemappingPlugin { pub struct TonemappingPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, + 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. @@ -294,22 +275,18 @@ 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: TONEMAPPING_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: ViewTarget::TEXTURE_FORMAT_HDR, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -340,6 +317,8 @@ impl FromWorld for TonemappingPipeline { TonemappingPipeline { texture_bind_group: tonemap_texture_bind_group, sampler, + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "tonemapping.wgsl"), } } } @@ -464,12 +443,9 @@ pub fn lut_placeholder() -> Image { let data = vec![255, 0, 255, 255]; Image { data: Some(data), + data_order: TextureDataOrder::default(), texture_descriptor: TextureDescriptor { - size: Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, + size: Extent3d::default(), format, dimension: TextureDimension::D3, label: None, @@ -481,5 +457,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..186b2ec820 --- /dev/null +++ b/crates/bevy_core_widgets/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "bevy_core_widgets" +version = "0.17.0-dev" +edition = "2024" +description = "Unstyled common widgets for Bevy Engine" +homepage = "https://bevy.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_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/callback.rs b/crates/bevy_core_widgets/src/callback.rs new file mode 100644 index 0000000000..37905e221c --- /dev/null +++ b/crates/bevy_core_widgets/src/callback.rs @@ -0,0 +1,113 @@ +use bevy_ecs::system::{Commands, SystemId, SystemInput}; +use bevy_ecs::world::{DeferredWorld, World}; + +/// A callback defines how we want to be notified when a widget changes state. Unlike an event +/// or observer, callbacks are intended for "point-to-point" communication that cuts across the +/// hierarchy of entities. Callbacks can be created in advance of the entity they are attached +/// to, and can be passed around as parameters. +/// +/// Example: +/// ``` +/// use bevy_app::App; +/// use bevy_core_widgets::{Callback, Notify}; +/// use bevy_ecs::system::{Commands, IntoSystem}; +/// +/// let mut app = App::new(); +/// +/// // Register a one-shot system +/// fn my_callback_system() { +/// println!("Callback executed!"); +/// } +/// +/// let system_id = app.world_mut().register_system(my_callback_system); +/// +/// // Wrap system in a callback +/// let callback = Callback::System(system_id); +/// +/// // Later, when we want to execute the callback: +/// app.world_mut().commands().notify(&callback); +/// ``` +#[derive(Default, Debug)] +pub enum Callback { + /// Invoke a one-shot system + System(SystemId), + /// Ignore this notification + #[default] + Ignore, +} + +/// Trait used to invoke a [`Callback`], unifying the API across callers. +pub trait Notify { + /// Invoke the callback with no arguments. + fn notify(&mut self, callback: &Callback<()>); + + /// Invoke the callback with one argument. + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static; +} + +impl<'w, 's> Notify for Commands<'w, 's> { + fn notify(&mut self, callback: &Callback<()>) { + match callback { + Callback::System(system_id) => self.run_system(*system_id), + Callback::Ignore => (), + } + } + + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static, + { + match callback { + Callback::System(system_id) => self.run_system_with(*system_id, input), + Callback::Ignore => (), + } + } +} + +impl Notify for World { + fn notify(&mut self, callback: &Callback<()>) { + match callback { + Callback::System(system_id) => { + let _ = self.run_system(*system_id); + } + Callback::Ignore => (), + } + } + + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static, + { + match callback { + Callback::System(system_id) => { + let _ = self.run_system_with(*system_id, input); + } + Callback::Ignore => (), + } + } +} + +impl Notify for DeferredWorld<'_> { + fn notify(&mut self, callback: &Callback<()>) { + match callback { + Callback::System(system_id) => { + self.commands().run_system(*system_id); + } + Callback::Ignore => (), + } + } + + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static, + { + match callback { + Callback::System(system_id) => { + self.commands().run_system_with(*system_id, input); + } + Callback::Ignore => (), + } + } +} 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..8c4ec9b22e --- /dev/null +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -0,0 +1,127 @@ +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}, +}; +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}; + +use crate::{Callback, Notify}; + +/// 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, Default, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] +pub struct CoreButton { + /// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key + /// is pressed while the button is focused. + pub on_activate: Callback, +} + +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) + { + trigger.propagate(false); + commands.notify(&bstate.on_activate); + } + } + } +} + +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 { + commands.notify(&bstate.on_activate); + } + } +} + +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..05edc53c44 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -0,0 +1,183 @@ +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}, +}; +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}; + +use crate::{Callback, Notify as _}; + +/// 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 `Callback::Ignore`, 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. If this value is + /// `Callback::Ignore`, then the checkbox will update it's own internal [`Checked`] state + /// without notification. + pub on_change: Callback>, +} + +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 !matches!(checkbox.on_change, Callback::Ignore) { + commands.notify_with(&checkbox.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..a6c99a0d04 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -0,0 +1,216 @@ +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}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_picking::events::{Click, Pointer}; +use bevy_ui::{Checkable, Checked, InteractionDisabled}; + +use crate::{Callback, Notify}; + +/// 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: Callback>, +} + +/// 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)), Checkable)] +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 + commands.notify_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 + } + }; + + // Radio button is disabled. + if q_radio.get(radio_id).unwrap().1 { + return; + } + + // 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 + commands.notify_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_scrollbar.rs b/crates/bevy_core_widgets/src/core_scrollbar.rs new file mode 100644 index 0000000000..d997f565ce --- /dev/null +++ b/crates/bevy_core_widgets/src/core_scrollbar.rs @@ -0,0 +1,329 @@ +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_ecs::{ + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + observer::On, + query::{With, Without}, + system::{Query, Res}, +}; +use bevy_math::Vec2; +use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press}; +use bevy_ui::{ + ComputedNode, ComputedNodeTarget, Node, ScrollPosition, UiGlobalTransform, UiScale, Val, +}; + +/// Used to select the orientation of a scrollbar, slider, or other oriented control. +// TODO: Move this to a more central place. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum ControlOrientation { + /// Horizontal orientation (stretching from left to right) + Horizontal, + /// Vertical orientation (stretching from top to bottom) + #[default] + Vertical, +} + +/// A headless scrollbar widget, which can be used to build custom scrollbars. +/// +/// Scrollbars operate differently than the other core widgets in a number of respects. +/// +/// Unlike sliders, scrollbars don't have an [`AccessibilityNode`](bevy_a11y::AccessibilityNode) +/// component, nor can they have keyboard focus. This is because scrollbars are usually used in +/// conjunction with a scrollable container, which is itself accessible and focusable. This also +/// means that scrollbars don't accept keyboard events, which is also the responsibility of the +/// scrollable container. +/// +/// Scrollbars don't emit notification events; instead they modify the scroll position of the target +/// entity directly. +/// +/// A scrollbar can have any number of child entities, but one entity must be the scrollbar thumb, +/// which is marked with the [`CoreScrollbarThumb`] component. Other children are ignored. The core +/// scrollbar will directly update the position and size of this entity; the application is free to +/// set any other style properties as desired. +/// +/// The application is free to position the scrollbars relative to the scrolling container however +/// it wants: it can overlay them on top of the scrolling content, or use a grid layout to displace +/// the content to make room for the scrollbars. +#[derive(Component, Debug)] +pub struct CoreScrollbar { + /// Entity being scrolled. + pub target: Entity, + /// Whether the scrollbar is vertical or horizontal. + pub orientation: ControlOrientation, + /// Minimum length of the scrollbar thumb, in pixel units, in the direction parallel to the main + /// scrollbar axis. The scrollbar will resize the thumb entity based on the proportion of + /// visible size to content size, but no smaller than this. This prevents the thumb from + /// disappearing in cases where the ratio of content size to visible size is large. + pub min_thumb_length: f32, +} + +/// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of +/// the scrollbar). This should be a child of the scrollbar entity. +#[derive(Component, Debug)] +#[require(CoreScrollbarDragState)] +pub struct CoreScrollbarThumb; + +impl CoreScrollbar { + /// Construct a new scrollbar. + /// + /// # Arguments + /// + /// * `target` - The scrollable entity that this scrollbar will control. + /// * `orientation` - The orientation of the scrollbar (horizontal or vertical). + /// * `min_thumb_length` - The minimum size of the scrollbar's thumb, in pixels. + pub fn new(target: Entity, orientation: ControlOrientation, min_thumb_length: f32) -> Self { + Self { + target, + orientation, + min_thumb_length, + } + } +} + +/// Component used to manage the state of a scrollbar during dragging. This component is +/// inserted on the thumb entity. +#[derive(Component, Default)] +pub struct CoreScrollbarDragState { + /// Whether the scrollbar is currently being dragged. + pub dragging: bool, + /// The value of the scrollbar when dragging started. + drag_origin: f32, +} + +fn scrollbar_on_pointer_down( + mut ev: On>, + q_thumb: Query<&ChildOf, With>, + mut q_scrollbar: Query<( + &CoreScrollbar, + &ComputedNode, + &ComputedNodeTarget, + &UiGlobalTransform, + )>, + mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, + ui_scale: Res, +) { + if q_thumb.contains(ev.target()) { + // If they click on the thumb, do nothing. This will be handled by the drag event. + ev.propagate(false); + } else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.target()) { + // If they click on the scrollbar track, page up or down. + ev.propagate(false); + + // Convert to widget-local coordinates. + let local_pos = transform.try_inverse().unwrap().transform_point2( + ev.event().pointer_location.position * node_target.scale_factor() / ui_scale.0, + ) + node.size() * 0.5; + + // Bail if we don't find the target entity. + let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else { + return; + }; + + // Convert the click coordinates into a scroll position. If it's greater than the + // current scroll position, scroll forward by one step (visible size) otherwise scroll + // back. + let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor; + let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor; + let max_range = (content_size - visible_size).max(Vec2::ZERO); + + fn adjust_scroll_pos(scroll_pos: &mut f32, click_pos: f32, step: f32, range: f32) { + *scroll_pos = + (*scroll_pos + if click_pos > *scroll_pos { step } else { -step }).clamp(0., range); + } + + match scrollbar.orientation { + ControlOrientation::Horizontal => { + if node.size().x > 0. { + let click_pos = local_pos.x * content_size.x / node.size().x; + adjust_scroll_pos(&mut scroll_pos.x, click_pos, visible_size.x, max_range.x); + } + } + ControlOrientation::Vertical => { + if node.size().y > 0. { + let click_pos = local_pos.y * content_size.y / node.size().y; + adjust_scroll_pos(&mut scroll_pos.y, click_pos, visible_size.y, max_range.y); + } + } + } + } +} + +fn scrollbar_on_drag_start( + mut ev: On>, + mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, + q_scrollbar: Query<&CoreScrollbar>, + q_scroll_area: Query<&ScrollPosition>, +) { + if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.target()) { + ev.propagate(false); + if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent) { + if let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) { + drag.dragging = true; + drag.drag_origin = match scrollbar.orientation { + ControlOrientation::Horizontal => scroll_area.x, + ControlOrientation::Vertical => scroll_area.y, + }; + } + } + } +} + +fn scrollbar_on_drag( + mut ev: On>, + mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, + mut q_scrollbar: Query<(&ComputedNode, &CoreScrollbar)>, + mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, + ui_scale: Res, +) { + if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.target()) { + if let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent) { + ev.propagate(false); + let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) + else { + return; + }; + + if drag.dragging { + let distance = ev.event().distance / ui_scale.0; + let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor; + let content_size = + scroll_content.content_size() * scroll_content.inverse_scale_factor; + let scrollbar_size = (node.size() * node.inverse_scale_factor).max(Vec2::ONE); + + match scrollbar.orientation { + ControlOrientation::Horizontal => { + let range = (content_size.x - visible_size.x).max(0.); + scroll_pos.x = (drag.drag_origin + + (distance.x * content_size.x) / scrollbar_size.x) + .clamp(0., range); + } + ControlOrientation::Vertical => { + let range = (content_size.y - visible_size.y).max(0.); + scroll_pos.y = (drag.drag_origin + + (distance.y * content_size.y) / scrollbar_size.y) + .clamp(0., range); + } + }; + } + } + } +} + +fn scrollbar_on_drag_end( + mut ev: On>, + mut q_thumb: Query<&mut CoreScrollbarDragState, With>, +) { + if let Ok(mut drag) = q_thumb.get_mut(ev.target()) { + ev.propagate(false); + if drag.dragging { + drag.dragging = false; + } + } +} + +fn scrollbar_on_drag_cancel( + mut ev: On>, + mut q_thumb: Query<&mut CoreScrollbarDragState, With>, +) { + if let Ok(mut drag) = q_thumb.get_mut(ev.target()) { + ev.propagate(false); + if drag.dragging { + drag.dragging = false; + } + } +} + +fn update_scrollbar_thumb( + q_scroll_area: Query<(&ScrollPosition, &ComputedNode)>, + q_scrollbar: Query<(&CoreScrollbar, &ComputedNode, &Children)>, + mut q_thumb: Query<&mut Node, With>, +) { + for (scrollbar, scrollbar_node, children) in q_scrollbar.iter() { + let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) else { + continue; + }; + + // Size of the visible scrolling area. + let visible_size = scroll_area.1.size() * scroll_area.1.inverse_scale_factor; + + // Size of the scrolling content. + let content_size = scroll_area.1.content_size() * scroll_area.1.inverse_scale_factor; + + // Length of the scrollbar track. + let track_length = scrollbar_node.size() * scrollbar_node.inverse_scale_factor; + + fn size_and_pos( + content_size: f32, + visible_size: f32, + track_length: f32, + min_size: f32, + offset: f32, + ) -> (f32, f32) { + let thumb_size = if content_size > visible_size { + (track_length * visible_size / content_size) + .max(min_size) + .min(track_length) + } else { + track_length + }; + + let thumb_pos = if content_size > visible_size { + offset * (track_length - thumb_size) / (content_size - visible_size) + } else { + 0. + }; + + (thumb_size, thumb_pos) + } + + for child in children { + if let Ok(mut thumb) = q_thumb.get_mut(*child) { + match scrollbar.orientation { + ControlOrientation::Horizontal => { + let (thumb_size, thumb_pos) = size_and_pos( + content_size.x, + visible_size.x, + track_length.x, + scrollbar.min_thumb_length, + scroll_area.0.x, + ); + + thumb.top = Val::Px(0.); + thumb.bottom = Val::Px(0.); + thumb.left = Val::Px(thumb_pos); + thumb.width = Val::Px(thumb_size); + } + ControlOrientation::Vertical => { + let (thumb_size, thumb_pos) = size_and_pos( + content_size.y, + visible_size.y, + track_length.y, + scrollbar.min_thumb_length, + scroll_area.0.y, + ); + + thumb.left = Val::Px(0.); + thumb.right = Val::Px(0.); + thumb.top = Val::Px(thumb_pos); + thumb.height = Val::Px(thumb_size); + } + }; + } + } + } +} + +/// Plugin that adds the observers for the [`CoreScrollbar`] widget. +pub struct CoreScrollbarPlugin; + +impl Plugin for CoreScrollbarPlugin { + fn build(&self, app: &mut App) { + app.add_observer(scrollbar_on_pointer_down) + .add_observer(scrollbar_on_drag_start) + .add_observer(scrollbar_on_drag_end) + .add_observer(scrollbar_on_drag_cancel) + .add_observer(scrollbar_on_drag) + .add_systems(PostUpdate, update_scrollbar_thumb); + } +} 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..521e6fc1d3 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -0,0 +1,493 @@ +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}, +}; +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}; + +use crate::{Callback, Notify}; + +/// 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 `Callback::Ignore`, then the slider will update it's own + /// internal [`SliderValue`] state without notification. + pub on_change: Callback>, + /// 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 { + /// The beginning of the allowed range for the slider value. + start: f32, + /// The end of the allowed range for the slider value. + 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 +/// shortcuts. 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 matches!(slider.on_change, Callback::Ignore) { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, 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 matches!(slider.on_change, Callback::Ignore) { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, 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 matches!(slider.on_change, Callback::Ignore) { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, 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 matches!(slider.on_change, Callback::Ignore) { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, 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..2a3fc1ac09 --- /dev/null +++ b/crates/bevy_core_widgets/src/lib.rs @@ -0,0 +1,53 @@ +//! 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 callback; +mod core_button; +mod core_checkbox; +mod core_radio; +mod core_scrollbar; +mod core_slider; + +use bevy_app::{App, Plugin}; + +pub use callback::{Callback, Notify}; +pub use core_button::{CoreButton, CoreButtonPlugin}; +pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; +pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin}; +pub use core_scrollbar::{ + ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin, + CoreScrollbarThumb, +}; +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, + CoreScrollbarPlugin, + CoreSliderPlugin, + )); + } +} diff --git a/crates/bevy_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml index 1c4cb4adcc..f1a1cd44b3 100644 --- a/crates/bevy_derive/Cargo.toml +++ b/crates/bevy_derive/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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_derive/compile_fail/Cargo.toml b/crates/bevy_derive/compile_fail/Cargo.toml index a9ad3e95e1..e9116dc57b 100644 --- a/crates/bevy_derive/compile_fail/Cargo.toml +++ b/crates/bevy_derive/compile_fail/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_derive_compile_fail" edition = "2024" description = "Compile fail tests for Bevy Engine's various macros" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" publish = false diff --git a/crates/bevy_derive/src/lib.rs b/crates/bevy_derive/src/lib.rs index 2636ffc57d..16a66eb906 100644 --- a/crates/bevy_derive/src/lib.rs +++ b/crates/bevy_derive/src/lib.rs @@ -1,9 +1,10 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +//! Assorted proc macro derive functions. + #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] extern crate proc_macro; @@ -188,11 +189,34 @@ pub fn derive_deref_mut(input: TokenStream) -> TokenStream { derefs::derive_deref_mut(input) } +/// Generates the required main function boilerplate for Android. #[proc_macro_attribute] pub fn bevy_main(attr: TokenStream, item: TokenStream) -> TokenStream { bevy_main::bevy_main(attr, item) } +/// Adds `enum_variant_index` and `enum_variant_name` functions to enums. +/// +/// # Example +/// +/// ``` +/// use bevy_derive::{EnumVariantMeta}; +/// +/// #[derive(EnumVariantMeta)] +/// enum MyEnum { +/// A, +/// B, +/// } +/// +/// let a = MyEnum::A; +/// let b = MyEnum::B; +/// +/// assert_eq!(0, a.enum_variant_index()); +/// assert_eq!("A", a.enum_variant_name()); +/// +/// assert_eq!(1, b.enum_variant_index()); +/// assert_eq!("B", b.enum_variant_name()); +/// ``` #[proc_macro_derive(EnumVariantMeta)] pub fn derive_enum_variant_meta(input: TokenStream) -> TokenStream { enum_variant_meta::derive_enum_variant_meta(input) @@ -205,8 +229,6 @@ pub fn derive_enum_variant_meta(input: TokenStream) -> TokenStream { pub fn derive_app_label(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); let mut trait_path = BevyManifest::shared().get_path("bevy_app"); - let mut dyn_eq_path = trait_path.clone(); trait_path.segments.push(format_ident!("AppLabel").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "AppLabel", &trait_path, &dyn_eq_path) + derive_label(input, "AppLabel", &trait_path) } diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 5f05af1d07..3f0efb1c21 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -13,26 +13,23 @@ 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_math = { path = "../bevy_math", 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_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_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..8e0b502aa0 100644 --- a/crates/bevy_dev_tools/src/ci_testing/systems.rs +++ b/crates/bevy_dev_tools/src/ci_testing/systems.rs @@ -18,9 +18,22 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { debug!("Handling event: {:?}", event); match event { CiTestingEvent::AppExit => { - world.send_event(AppExit::Success); + world.write_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)); @@ -40,7 +53,7 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { } // Custom events are forwarded to the world. CiTestingEvent::Custom(event_string) => { - world.send_event(CiTestingCustomEvent(event_string)); + world.write_event(CiTestingCustomEvent(event_string)); } } } diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index 387acc5989..8efea87f00 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -1,11 +1,11 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] -//! This crate provides additional utilities for the [Bevy game engine](https://bevyengine.org), +//! This crate provides additional utilities for the [Bevy game engine](https://bevy.org), //! focused on improving developer experience. use bevy_app::prelude::*; diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs index 8e4ee7ae86..16233cd3dc 100644 --- a/crates/bevy_dev_tools/src/picking_debug.rs +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -1,14 +1,13 @@ //! Text and on-screen debugging tools use bevy_app::prelude::*; -use bevy_asset::prelude::*; 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::*; @@ -92,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::, @@ -122,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:?}"); } @@ -215,7 +214,7 @@ pub fn update_debug_data( entity_names: Query, mut pointers: Query<( &PointerId, - &pointer::PointerLocation, + &PointerLocation, &PointerPress, &mut PointerDebug, )>, @@ -248,25 +247,18 @@ pub fn debug_draw( pointers: Query<(Entity, &PointerId, &PointerDebug)>, scale: Res, ) { - let font_handle: Handle = Default::default(); - for (entity, id, debug) in pointers.iter() { + for (entity, id, debug) in &pointers { let Some(pointer_location) = &debug.location else { continue; }; let text = format!("{id:?}\n{debug}"); - for camera in camera_query - .iter() - .map(|(entity, camera)| { - ( - entity, - camera.target.normalize(primary_window.single().ok()), - ) - }) - .filter_map(|(entity, target)| Some(entity).zip(target)) - .filter(|(_entity, target)| target == &pointer_location.target) - .map(|(cam_entity, _target)| cam_entity) - { + for (camera, _) in camera_query.iter().filter(|(_, camera)| { + camera + .target + .normalize(primary_window.single().ok()) + .is_some_and(|target| target == pointer_location.target) + }) { let mut pointer_pos = pointer_location.position; if let Some(viewport) = camera_query .get(camera) @@ -278,23 +270,21 @@ pub fn debug_draw( commands .entity(entity) + .despawn_related::() .insert(( - Text::new(text.clone()), - TextFont { - font: font_handle.clone(), - font_size: 12.0, - ..Default::default() - }, - TextColor(Color::WHITE), Node { position_type: PositionType::Absolute, left: Val::Px(pointer_pos.x + 5.0) / scale.0, top: Val::Px(pointer_pos.y + 5.0) / scale.0, + padding: UiRect::px(10.0, 10.0, 8.0, 6.0), ..Default::default() }, - )) - .insert(Pickable::IGNORE) - .insert(UiTargetCamera(camera)); + BackgroundColor(Color::BLACK.with_alpha(0.75)), + GlobalZIndex(i32::MAX), + Pickable::IGNORE, + UiTargetCamera(camera), + children![(Text::new(text.clone()), TextFont::from_font_size(12.0))], + )); } } } diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index eff1710438..424ca67437 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -18,7 +18,6 @@ serialize = [ "dep:serde", "bevy_ecs/serialize", "bevy_time/serialize", - "bevy_utils/serde", "bevy_platform/serialize", ] @@ -39,7 +38,6 @@ std = [ "bevy_app/std", "bevy_platform/std", "bevy_time/std", - "bevy_utils/std", "bevy_tasks/std", ] @@ -50,20 +48,16 @@ critical-section = [ "bevy_app/critical-section", "bevy_platform/critical-section", "bevy_time/critical-section", - "bevy_utils/critical-section", "bevy_tasks/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, features = [ - "alloc", -] } -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_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/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index 00a758416b..1f67d5220a 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -113,7 +113,9 @@ impl core::fmt::Display for DiagnosticPath { /// A single measurement of a [`Diagnostic`]. #[derive(Debug)] pub struct DiagnosticMeasurement { + /// When this measurement was taken. pub time: Instant, + /// Value of the measurement. pub value: f64, } @@ -122,12 +124,14 @@ pub struct DiagnosticMeasurement { #[derive(Debug)] pub struct Diagnostic { path: DiagnosticPath, + /// Suffix to use when logging measurements for this [`Diagnostic`], for example to show units. pub suffix: Cow<'static, str>, history: VecDeque, sum: f64, ema: f64, ema_smoothing_factor: f64, max_history_length: usize, + /// Disabled [`Diagnostic`]s are not measured or logged. pub is_enabled: bool, } @@ -219,6 +223,7 @@ impl Diagnostic { self } + /// Get the [`DiagnosticPath`] that identifies this [`Diagnostic`]. pub fn path(&self) -> &DiagnosticPath { &self.path } @@ -282,10 +287,12 @@ impl Diagnostic { self.max_history_length } + /// All measured values from this [`Diagnostic`], up to the configured maximum history length. pub fn values(&self) -> impl Iterator { self.history.iter().map(|x| &x.value) } + /// All measurements from this [`Diagnostic`], up to the configured maximum history length. pub fn measurements(&self) -> impl Iterator { self.history.iter() } @@ -293,6 +300,8 @@ impl Diagnostic { /// Clear the history of this diagnostic. pub fn clear_history(&mut self) { self.history.clear(); + self.sum = 0.0; + self.ema = 0.0; } } @@ -310,10 +319,12 @@ impl DiagnosticsStore { self.diagnostics.insert(diagnostic.path.clone(), diagnostic); } + /// Get the [`DiagnosticMeasurement`] with the given [`DiagnosticPath`], if it exists. pub fn get(&self, path: &DiagnosticPath) -> Option<&Diagnostic> { self.diagnostics.get(path) } + /// Mutably get the [`DiagnosticMeasurement`] with the given [`DiagnosticPath`], if it exists. pub fn get_mut(&mut self, path: &DiagnosticPath) -> Option<&mut Diagnostic> { self.diagnostics.get_mut(path) } @@ -420,3 +431,31 @@ impl RegisterDiagnostic for App { self } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clear_history() { + const MEASUREMENT: f64 = 20.0; + + let mut diagnostic = + Diagnostic::new(DiagnosticPath::new("test")).with_max_history_length(5); + let mut now = Instant::now(); + + for _ in 0..3 { + for _ in 0..5 { + diagnostic.add_measurement(DiagnosticMeasurement { + time: now, + value: MEASUREMENT, + }); + // Increase time to test smoothed average. + now += Duration::from_secs(1); + } + assert!((diagnostic.average().unwrap() - MEASUREMENT).abs() < 0.1); + assert!((diagnostic.smoothed().unwrap() - MEASUREMENT).abs() < 0.1); + diagnostic.clear_history(); + } + } +} diff --git a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs index 91874a390c..b20a82bf6c 100644 --- a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs @@ -19,8 +19,10 @@ impl Plugin for EntityCountDiagnosticsPlugin { } impl EntityCountDiagnosticsPlugin { + /// Number of currently allocated entities. pub const ENTITY_COUNT: DiagnosticPath = DiagnosticPath::const_new("entity_count"); + /// Updates entity count measurement. pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) { diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.len() as f64); } diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index 22b6176fa2..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`. @@ -58,10 +60,16 @@ impl Plugin for FrameTimeDiagnosticsPlugin { } impl FrameTimeDiagnosticsPlugin { + /// Frames per second. pub const FPS: DiagnosticPath = DiagnosticPath::const_new("fps"); + + /// Total frames since application start. pub const FRAME_COUNT: DiagnosticPath = DiagnosticPath::const_new("frame_count"); + + /// Frame time in ms. pub const FRAME_TIME: DiagnosticPath = DiagnosticPath::const_new("frame_time"); + /// Updates frame count, frame time and fps measurements. pub fn diagnostic_system( mut diagnostics: Diagnostics, time: Res>, diff --git a/crates/bevy_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs index e5098d6c6f..707c7c7cc7 100644 --- a/crates/bevy_diagnostic/src/lib.rs +++ b/crates/bevy_diagnostic/src/lib.rs @@ -1,13 +1,12 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] -//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevyengine.org/). +//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevy.org/). //! It allows users to easily add diagnostic functionality to their Bevy applications, enhancing //! their ability to monitor and optimize their game's. @@ -29,7 +28,7 @@ pub use diagnostic::*; pub use entity_count_diagnostics_plugin::EntityCountDiagnosticsPlugin; pub use frame_count_diagnostics_plugin::{update_frame_count, FrameCount, FrameCountPlugin}; pub use frame_time_diagnostics_plugin::FrameTimeDiagnosticsPlugin; -pub use log_diagnostics_plugin::LogDiagnosticsPlugin; +pub use log_diagnostics_plugin::{LogDiagnosticsPlugin, LogDiagnosticsState}; #[cfg(feature = "sysinfo_plugin")] pub use system_information_diagnostics_plugin::{SystemInfo, SystemInformationDiagnosticsPlugin}; diff --git a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs index 1246b03f81..f6888ea723 100644 --- a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs @@ -1,7 +1,8 @@ use super::{Diagnostic, DiagnosticPath, DiagnosticsStore}; -use alloc::vec::Vec; + use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use bevy_platform::collections::HashSet; use bevy_time::{Real, Time, Timer, TimerMode}; use core::time::Duration; use log::{debug, info}; @@ -14,16 +15,76 @@ use log::{debug, info}; /// /// When no diagnostics are provided, this plugin does nothing. pub struct LogDiagnosticsPlugin { + /// If `true` then the `Debug` representation of each `Diagnostic` is logged. + /// If `false` then a (smoothed) current value and historical average are logged. + /// + /// Defaults to `false`. pub debug: bool, + /// Time to wait between logging diagnostics and logging them again. pub wait_duration: Duration, - pub filter: Option>, + /// If `Some` then only these diagnostics are logged. + pub filter: Option>, } /// State used by the [`LogDiagnosticsPlugin`] #[derive(Resource)] -struct LogDiagnosticsState { +pub struct LogDiagnosticsState { timer: Timer, - filter: Option>, + filter: Option>, +} + +impl LogDiagnosticsState { + /// Sets a new duration for the log timer + pub fn set_timer_duration(&mut self, duration: Duration) { + self.timer.set_duration(duration); + self.timer.set_elapsed(Duration::ZERO); + } + + /// Add a filter to the log state, returning `true` if the [`DiagnosticPath`] + /// was not present + pub fn add_filter(&mut self, diagnostic_path: DiagnosticPath) -> bool { + if let Some(filter) = &mut self.filter { + filter.insert(diagnostic_path) + } else { + self.filter = Some(HashSet::from_iter([diagnostic_path])); + true + } + } + + /// Extends the filter of the log state with multiple [`DiagnosticPaths`](DiagnosticPath) + pub fn extend_filter(&mut self, iter: impl IntoIterator) { + if let Some(filter) = &mut self.filter { + filter.extend(iter); + } else { + self.filter = Some(HashSet::from_iter(iter)); + } + } + + /// Removes a filter from the log state, returning `true` if it was present + pub fn remove_filter(&mut self, diagnostic_path: &DiagnosticPath) -> bool { + if let Some(filter) = &mut self.filter { + filter.remove(diagnostic_path) + } else { + false + } + } + + /// Clears the filters of the log state + pub fn clear_filter(&mut self) { + if let Some(filter) = &mut self.filter { + filter.clear(); + } + } + + /// Enables filtering with empty filters + pub fn enable_filtering(&mut self) { + self.filter = Some(HashSet::new()); + } + + /// Disables filtering + pub fn disable_filtering(&mut self) { + self.filter = None; + } } impl Default for LogDiagnosticsPlugin { @@ -52,7 +113,8 @@ impl Plugin for LogDiagnosticsPlugin { } impl LogDiagnosticsPlugin { - pub fn filtered(filter: Vec) -> Self { + /// Filter logging to only the paths in `filter`. + pub fn filtered(filter: HashSet) -> Self { LogDiagnosticsPlugin { filter: Some(filter), ..Default::default() @@ -65,7 +127,7 @@ impl LogDiagnosticsPlugin { mut callback: impl FnMut(&Diagnostic), ) { if let Some(filter) = &state.filter { - for path in filter { + for path in filter.iter() { if let Some(diagnostic) = diagnostics.get(path) { if diagnostic.is_enabled { callback(diagnostic); @@ -128,7 +190,7 @@ impl LogDiagnosticsPlugin { time: Res>, diagnostics: Res, ) { - if state.timer.tick(time.delta()).finished() { + if state.timer.tick(time.delta()).is_finished() { Self::log_diagnostics(&state, &diagnostics); } } @@ -138,9 +200,9 @@ impl LogDiagnosticsPlugin { time: Res>, diagnostics: Res, ) { - if state.timer.tick(time.delta()).finished() { + 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_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 376a109ae3..83d3663895 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -46,10 +46,15 @@ impl SystemInformationDiagnosticsPlugin { /// [`SystemInformationDiagnosticsPlugin`] for more information. #[derive(Debug, Resource)] pub struct SystemInfo { + /// OS name and version. pub os: String, + /// System kernel version. pub kernel: String, + /// CPU model name. pub cpu: String, + /// Physical core count. pub core_count: String, + /// System RAM. pub memory: String, } @@ -231,7 +236,7 @@ pub mod internal { memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB), }; - info!("{:?}", system_info); + info!("{system_info:?}"); system_info } } diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml index 26aec33b83..334ea10f1f 100644 --- a/crates/bevy_dylib/Cargo.toml +++ b/crates/bevy_dylib/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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_dylib/src/lib.rs b/crates/bevy_dylib/src/lib.rs index 1ff40ce3e8..84322813db 100644 --- a/crates/bevy_dylib/src/lib.rs +++ b/crates/bevy_dylib/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Forces dynamic linking of Bevy. diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 97cdcee082..f0f9b782af 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -1,14 +1,14 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["ecs", "game", "bevy"] categories = ["game-engines", "data-structures"] -rust-version = "1.85.0" +rust-version = "1.86.0" [features] default = ["std", "bevy_reflect", "async_executor", "backtrace"] @@ -20,12 +20,7 @@ default = ["std", "bevy_reflect", "async_executor", "backtrace"] multi_threaded = ["bevy_tasks/multi_threaded", "dep:arrayvec"] ## Adds serialization support through `serde`. -serialize = [ - "dep:serde", - "bevy_utils/serde", - "bevy_platform/serialize", - "indexmap/serde", -] +serialize = ["dep:serde", "bevy_platform/serialize", "indexmap/serde"] ## Adds runtime reflection support using `bevy_reflect`. bevy_reflect = ["dep:bevy_reflect"] @@ -33,13 +28,6 @@ bevy_reflect = ["dep:bevy_reflect"] ## Extends reflection support to functions. reflect_functions = ["bevy_reflect", "bevy_reflect/functions"] -## Use the configurable global error handler as the default error handler. -## -## This is typically used to turn panics from the ECS into loggable errors. -## This may be useful for production builds, -## but can result in a measurable performance impact, especially for commands. -configurable_error_handler = [] - ## Enables automatic backtrace capturing in BevyError backtrace = ["std"] @@ -47,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"] @@ -74,10 +62,10 @@ async_executor = ["std", "bevy_tasks/async_executor"] 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", @@ -95,29 +83,28 @@ critical-section = [ "bevy_reflect?/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, features = [ - "alloc", -] } -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", @@ -125,12 +112,17 @@ derive_more = { version = "1", default-features = false, features = [ ] } nonmax = { version = "0.5", default-features = false } arrayvec = { version = "0.7.4", default-features = false, optional = true } -smallvec = { version = "1", features = ["union", "const_generics"] } +smallvec = { version = "1", default-features = false, features = [ + "union", + "const_generics", +] } indexmap = { version = "2.5.0", default-features = false } variadics_please = { version = "1.1", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } bumpalo = "3" +subsecond = { version = "0.7.0-alpha.1", optional = true } +slotmap = { version = "1.0.7", default-features = false } concurrent-queue = { version = "2.5.0", default-features = false } [target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies] diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index c2fdc53d05..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,37 +307,39 @@ 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| { +world.add_observer(|trigger: On, mut commands: Commands| { println!("Entity {} goes BOOM!", trigger.target()); commands.entity(trigger.target()).despawn(); }); @@ -349,4 +349,4 @@ world.flush(); world.trigger_targets(Explode, entity); ``` -[bevy]: https://bevyengine.org/ +[bevy]: https://bevy.org/ diff --git a/crates/bevy_ecs/compile_fail/Cargo.toml b/crates/bevy_ecs/compile_fail/Cargo.toml index 48e3857f53..96c48ac6a3 100644 --- a/crates/bevy_ecs/compile_fail/Cargo.toml +++ b/crates/bevy_ecs/compile_fail/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_ecs_compile_fail" edition = "2024" description = "Compile fail tests for Bevy Engine's entity component system" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" publish = false diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs index 4076819ee3..79c2158644 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs @@ -60,4 +60,4 @@ mod case4 { pub struct BarTargetOf(Entity); } -fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {} +fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::lifecycle::HookContext) {} diff --git a/crates/bevy_ecs/examples/change_detection.rs b/crates/bevy_ecs/examples/change_detection.rs index 820860070c..42611e57e1 100644 --- a/crates/bevy_ecs/examples/change_detection.rs +++ b/crates/bevy_ecs/examples/change_detection.rs @@ -84,7 +84,7 @@ fn print_changed_entities( entity_with_mutated_component: Query<(Entity, &Age), Changed>, ) { for entity in &entity_with_added_component { - println!(" {entity} has it's first birthday!"); + println!(" {entity} has its first birthday!"); } for (entity, value) in &entity_with_mutated_component { println!(" {entity} is now {value:?} frames old"); 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 e87a4b650b..1322022581 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -13,14 +13,12 @@ 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 mut auto_propagate = false; - let mut traversal: Type = parse_quote!(()); let bevy_ecs_path: Path = crate::bevy_ecs_path(); ast.generics @@ -28,17 +26,43 @@ pub fn derive_event(input: TokenStream) -> TokenStream { .predicates .push(parse_quote! { Self: Send + Sync + 'static }); - if let Some(attr) = ast.attrs.iter().find(|attr| attr.path().is_ident(EVENT)) { + 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!(()); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + let mut processed_attrs = Vec::new(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Send + Sync + 'static }); + + for attr in ast.attrs.iter().filter(|attr| attr.path().is_ident(EVENT)) { if let Err(e) = attr.parse_nested_meta(|meta| match meta.path.get_ident() { + Some(ident) if processed_attrs.iter().any(|i| ident == i) => { + Err(meta.error(format!("duplicate attribute: {ident}"))) + } Some(ident) if ident == AUTO_PROPAGATE => { auto_propagate = true; + processed_attrs.push(AUTO_PROPAGATE); Ok(()) } Some(ident) if ident == TRAVERSAL => { traversal = meta.value()?.parse()?; + processed_attrs.push(TRAVERSAL); Ok(()) } - Some(ident) => Err(meta.error(format!("unsupported attribute: {}", ident))), + Some(ident) => Err(meta.error(format!("unsupported attribute: {ident}"))), None => Err(meta.error("expected identifier")), }) { return e.to_compile_error().into(); @@ -49,13 +73,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(); @@ -74,6 +115,7 @@ pub fn derive_resource(input: TokenStream) -> TokenStream { }) } +/// Component derive syntax is documented on both the macro and the trait. pub fn derive_component(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); @@ -94,9 +136,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 +349,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,11 +449,16 @@ 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"; -/// All allowed attribute value expression kinds for component hooks +/// All allowed attribute value expression kinds for component hooks. +/// This doesn't simply use general expressions because of conflicting needs: +/// - we want to be able to use `Self` & generic parameters in paths +/// - call expressions producing a closure need to be wrapped in a function +/// to turn them into function pointers, which prevents access to the outer generic params #[derive(Debug)] enum HookAttributeKind { /// expressions like function or struct names @@ -434,7 +492,7 @@ impl HookAttributeKind { HookAttributeKind::Path(path) => path.to_token_stream(), HookAttributeKind::Call(call) => { quote!({ - fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) { + fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::lifecycle::HookContext) { (#call)(world, ctx) } _internal_hook @@ -450,6 +508,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 +570,7 @@ struct Attrs { relationship_target: Option, immutable: bool, clone_behavior: Option, + map_entities: Option, } #[derive(Clone, Copy)] @@ -501,6 +610,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 +649,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")) } @@ -658,7 +771,7 @@ fn hook_register_function_call( ) -> Option { function.map(|meta| { quote! { - fn #hook() -> ::core::option::Option<#bevy_ecs_path::component::ComponentHook> { + fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> { ::core::option::Option::Some(#meta) } } @@ -758,6 +871,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 a1898b1328..7b388f4a14 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -1,4 +1,5 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +//! Macros for deriving ECS traits. + #![cfg_attr(docsrs, feature(doc_auto_cfg))] extern crate proc_macro; @@ -15,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, @@ -28,13 +29,49 @@ enum BundleFieldKind { const BUNDLE_ATTRIBUTE_NAME: &str = "bundle"; const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore"; +const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components"; +#[derive(Debug)] +struct BundleAttributes { + impl_from_components: bool, +} + +impl Default for BundleAttributes { + fn default() -> Self { + Self { + impl_from_components: true, + } + } +} + +/// Implement the `Bundle` trait. #[proc_macro_derive(Bundle, attributes(bundle))] pub fn derive_bundle(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ecs_path = bevy_ecs_path(); - let named_fields = match get_struct_fields(&ast.data) { + let mut errors = vec![]; + + let mut attributes = BundleAttributes::default(); + + for attr in &ast.attrs { + if attr.path().is_ident(BUNDLE_ATTRIBUTE_NAME) { + let parsing = attr.parse_nested_meta(|meta| { + if meta.path.is_ident(BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS) { + attributes.impl_from_components = false; + return Ok(()); + } + + Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`"))) + }); + + if let Err(error) = parsing { + errors.push(error.into_compile_error()); + } + } + } + + let named_fields = match get_struct_fields(&ast.data, "derive(Bundle)") { Ok(fields) => fields, Err(e) => return e.into_compile_error().into(), }; @@ -42,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() @@ -49,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!( @@ -61,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { } } - field_kind.push(BundleFieldKind::Component); + field_kind.push(kind); } let field = named_fields @@ -74,61 +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; - TokenStream::from(quote! { + 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 @@ -138,40 +149,20 @@ 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)* - } - - fn register_required_components( - components: &mut #ecs_path::component::ComponentsRegistrator, - required_components: &mut #ecs_path::component::RequiredComponents - ){ - #(#field_required_components)* - } - } - - // 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)* - } + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)* } } + }; + let dynamic_bundle_impl = quote! { #[allow(deprecated)] impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { type Effect = (); @@ -181,22 +172,54 @@ 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 }) } +/// Implement the `MapEntities` trait. #[proc_macro_derive(MapEntities, attributes(entities))] pub fn derive_map_entities(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ecs_path = bevy_ecs_path(); + let map_entities_impl = map_entities( &ast.data, + &ecs_path, Ident::new("self", Span::call_site()), false, false, + None, ); + let struct_name = &ast.ident; let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { @@ -240,7 +263,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { .as_ref() .map(|f| quote! { #f }) .unwrap_or_else(|| quote! { #i }); - field_names.push(format!("::{}", field_value)); + field_names.push(format!("::{field_value}")); fields.push(field_value); field_types.push(&field.ty); let mut field_message = None; @@ -392,10 +415,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) } } } @@ -424,15 +447,14 @@ 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), } } - unsafe fn new_archetype(state: &mut Self::State, archetype: &#path::archetype::Archetype, system_meta: &mut #path::system::SystemMeta) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::new_archetype(&mut state.state, archetype, system_meta) } + 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) { @@ -445,7 +467,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { #[inline] unsafe fn validate_param<'w, 's>( - state: &'s Self::State, + state: &'s mut Self::State, _system_meta: &#path::system::SystemMeta, _world: #path::world::unsafe_world_cell::UnsafeWorldCell<'w>, ) -> Result<(), #path::system::SystemParamValidationError> { @@ -503,12 +525,10 @@ pub fn derive_schedule_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let mut trait_path = bevy_ecs_path(); trait_path.segments.push(format_ident!("schedule").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path .segments .push(format_ident!("ScheduleLabel").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "ScheduleLabel", &trait_path, &dyn_eq_path) + derive_label(input, "ScheduleLabel", &trait_path) } /// Derive macro generating an impl of the trait `SystemSet`. @@ -519,26 +539,131 @@ pub fn derive_system_set(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let mut trait_path = bevy_ecs_path(); trait_path.segments.push(format_ident!("schedule").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path.segments.push(format_ident!("SystemSet").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "SystemSet", &trait_path, &dyn_eq_path) + derive_label(input, "SystemSet", &trait_path) } pub(crate) fn bevy_ecs_path() -> syn::Path { BevyManifest::shared().get_path("bevy_ecs") } -#[proc_macro_derive(Event, attributes(event))] +/// Implement the `Event` trait. +#[proc_macro_derive(Event)] pub fn derive_event(input: TokenStream) -> TokenStream { component::derive_event(input) } +/// Cheat sheet for derive syntax, +/// see full explanation on `EntityEvent` trait docs. +/// +/// ```ignore +/// #[derive(Event, EntityEvent)] +/// /// Traversal component +/// #[entity_event(traversal = &'static ChildOf)] +/// /// Always propagate +/// #[entity_event(auto_propagate)] +/// struct MyEvent; +/// ``` +#[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 { component::derive_resource(input) } +/// Cheat sheet for derive syntax, +/// see full explanation and examples on the `Component` trait doc. +/// +/// ## Immutability +/// ```ignore +/// #[derive(Component)] +/// #[component(immutable)] +/// struct MyComponent; +/// ``` +/// +/// ## Sparse instead of table-based storage +/// ```ignore +/// #[derive(Component)] +/// #[component(storage = "SparseSet")] +/// struct MyComponent; +/// ``` +/// +/// ## Required Components +/// +/// ```ignore +/// #[derive(Component)] +/// #[require( +/// // `Default::default()` +/// A, +/// // tuple structs +/// B(1), +/// // named-field structs +/// C { +/// x: 1, +/// ..default() +/// }, +/// // unit structs/variants +/// D::One, +/// // associated consts +/// E::ONE, +/// // constructors +/// F::new(1), +/// // arbitrary expressions +/// G = make(1, 2, 3) +/// )] +/// struct MyComponent; +/// ``` +/// +/// ## Relationships +/// ```ignore +/// #[derive(Component)] +/// #[relationship(relationship_target = Children)] +/// pub struct ChildOf { +/// // Marking the field is not necessary if there is only one. +/// #[relationship] +/// pub parent: Entity, +/// internal: u8, +/// }; +/// +/// #[derive(Component)] +/// #[relationship_target(relationship = ChildOf)] +/// pub struct Children(Vec); +/// ``` +/// +/// On despawn, also despawn all related entities: +/// ```ignore +/// #[derive(Component)] +/// #[relationship_target(relationship_target = Children, linked_spawn)] +/// pub struct Children(Vec); +/// ``` +/// +/// ## Hooks +/// ```ignore +/// #[derive(Component)] +/// #[component(hook_name = function)] +/// struct MyComponent; +/// ``` +/// where `hook_name` is `on_add`, `on_insert`, `on_replace` or `on_remove`; +/// `function` can be either a path, e.g. `some_function::`, +/// or a function call that returns a function that can be turned into +/// a `ComponentHook`, e.g. `get_closure("Hi!")`. +/// +/// ## Ignore this component when cloning an entity +/// ```ignore +/// #[derive(Component)] +/// #[component(clone_behavior = Ignore)] +/// struct MyComponent; +/// ``` #[proc_macro_derive( Component, attributes(component, require, relationship, relationship_target, entities) @@ -547,6 +672,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { component::derive_component(input) } +/// Implement the `FromWorld` trait. #[proc_macro_derive(FromWorld, attributes(from_world))] pub fn derive_from_world(input: TokenStream) -> TokenStream { let bevy_ecs_path = bevy_ecs_path(); 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 77ee532a50..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 @@ -92,7 +92,7 @@ pub(crate) fn world_query_impl( } } - // SAFETY: `update_component_access` and `update_archetype_component_access` are called on every field + // SAFETY: `update_component_access` is called on every field unsafe impl #user_impl_generics #path::query::WorldQuery for #struct_name #user_ty_generics #user_where_clauses { @@ -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 2468c5e9a8..f682554ce9 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -23,15 +23,21 @@ use crate::{ bundle::BundleId, component::{ComponentId, Components, RequiredComponentConstructor, StorageType}, entity::{Entity, EntityLocation}, + event::Event, observer::Observers, - storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow}, + storage::{ImmutableSparseSet, SparseArray, SparseSet, TableId, TableRow}, }; use alloc::{boxed::Box, vec::Vec}; -use bevy_platform::collections::HashMap; +use bevy_platform::collections::{hash_map::Entry, HashMap}; use core::{ hash::Hash, ops::{Index, IndexMut, RangeFrom}, }; +use nonmax::NonMaxU32; + +#[derive(Event)] +#[expect(dead_code, reason = "Prepare for the upcoming Query as Entities")] +pub(crate) struct ArchetypeCreated(pub ArchetypeId); /// An opaque location within a [`Archetype`]. /// @@ -44,23 +50,30 @@ use core::{ #[derive(Debug, Copy, Clone, Eq, PartialEq)] // SAFETY: Must be repr(transparent) due to the safety requirements on EntityLocation #[repr(transparent)] -pub struct ArchetypeRow(u32); +pub struct ArchetypeRow(NonMaxU32); impl ArchetypeRow { /// Index indicating an invalid archetype row. /// This is meant to be used as a placeholder. - pub const INVALID: ArchetypeRow = ArchetypeRow(u32::MAX); + // TODO: Deprecate in favor of options, since `INVALID` is, technically, valid. + pub const INVALID: ArchetypeRow = ArchetypeRow(NonMaxU32::MAX); /// Creates a `ArchetypeRow`. #[inline] - pub const fn new(index: usize) -> Self { - Self(index as u32) + pub const fn new(index: NonMaxU32) -> Self { + Self(index) } /// Gets the index of the row. #[inline] pub const fn index(self) -> usize { - self.0 as usize + self.0.get() as usize + } + + /// Gets the index of the row. + #[inline] + pub const fn index_u32(self) -> u32 { + self.0.get() } } @@ -341,7 +354,6 @@ pub(crate) struct ArchetypeSwapRemoveResult { /// [`Component`]: crate::component::Component struct ArchetypeComponentInfo { storage_type: StorageType, - archetype_component_id: ArchetypeComponentId, } bitflags::bitflags! { @@ -386,14 +398,14 @@ impl Archetype { observers: &Observers, id: ArchetypeId, table_id: TableId, - table_components: impl Iterator, - sparse_set_components: impl Iterator, + table_components: impl Iterator, + sparse_set_components: impl Iterator, ) -> Self { let (min_table, _) = table_components.size_hint(); let (min_sparse, _) = sparse_set_components.size_hint(); let mut flags = ArchetypeFlags::empty(); let mut archetype_components = SparseSet::with_capacity(min_table + min_sparse); - for (idx, (component_id, archetype_component_id)) in table_components.enumerate() { + for (idx, component_id) in table_components.enumerate() { // SAFETY: We are creating an archetype that includes this component so it must exist let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); @@ -402,7 +414,6 @@ impl Archetype { component_id, ArchetypeComponentInfo { storage_type: StorageType::Table, - archetype_component_id, }, ); // NOTE: the `table_components` are sorted AND they were inserted in the `Table` in the same @@ -414,7 +425,7 @@ impl Archetype { .insert(id, ArchetypeRecord { column: Some(idx) }); } - for (component_id, archetype_component_id) in sparse_set_components { + for component_id in sparse_set_components { // SAFETY: We are creating an archetype that includes this component so it must exist let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); @@ -423,7 +434,6 @@ impl Archetype { component_id, ArchetypeComponentInfo { storage_type: StorageType::SparseSet, - archetype_component_id, }, ); component_index @@ -467,6 +477,27 @@ impl Archetype { &self.entities } + /// Fetches the entities contained in this archetype. + #[inline] + pub fn entities_with_location(&self) -> impl Iterator { + self.entities.iter().enumerate().map( + |(archetype_row, &ArchetypeEntity { entity, table_row })| { + ( + entity, + EntityLocation { + archetype_id: self.id, + // SAFETY: The entities in the archetype must be unique and there are never more than u32::MAX entities. + archetype_row: unsafe { + ArchetypeRow::new(NonMaxU32::new_unchecked(archetype_row as u32)) + }, + table_id: self.table_id, + table_row, + }, + ) + }, + ) + } + /// Gets an iterator of all of the components stored in [`Table`]s. /// /// All of the IDs are unique. @@ -507,16 +538,6 @@ impl Archetype { self.components.len() } - /// Gets an iterator of all of the components in the archetype, along with - /// their archetype component ID. - pub(crate) fn components_with_archetype_component_id( - &self, - ) -> impl Iterator + '_ { - self.components - .iter() - .map(|(component_id, info)| (*component_id, info.archetype_component_id)) - } - /// Fetches an immutable reference to the archetype's [`Edges`], a cache of /// archetypal relationships. #[inline] @@ -569,7 +590,8 @@ impl Archetype { entity: Entity, table_row: TableRow, ) -> EntityLocation { - let archetype_row = ArchetypeRow::new(self.entities.len()); + // SAFETY: An entity can not have multiple archetype rows and there can not be more than u32::MAX entities. + let archetype_row = unsafe { ArchetypeRow::new(NonMaxU32::new_unchecked(self.len())) }; self.entities.push(ArchetypeEntity { entity, table_row }); EntityLocation { @@ -606,8 +628,10 @@ impl Archetype { /// Gets the total number of entities that belong to the archetype. #[inline] - pub fn len(&self) -> usize { - self.entities.len() + pub fn len(&self) -> u32 { + // No entity may have more than one archetype row, so there are no duplicates, + // and there may only ever be u32::MAX entities, so the length never exceeds u32's capacity. + self.entities.len() as u32 } /// Checks if the archetype has any entities. @@ -632,19 +656,6 @@ impl Archetype { .map(|info| info.storage_type) } - /// Fetches the corresponding [`ArchetypeComponentId`] for a component in the archetype. - /// Returns `None` if the component is not part of the archetype. - /// This runs in `O(1)` time. - #[inline] - pub fn get_archetype_component_id( - &self, - component_id: ComponentId, - ) -> Option { - self.components - .get(component_id) - .map(|info| info.archetype_component_id) - } - /// Clears all entities from the archetype. pub(crate) fn clear_entities(&mut self) { self.entities.clear(); @@ -680,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::world::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::world::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::world::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::world::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::world::OnDespawn + /// [`Despawn`]: crate::lifecycle::Despawn #[inline] pub fn has_despawn_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER) @@ -742,46 +753,6 @@ struct ArchetypeComponents { sparse_set_components: Box<[ComponentId]>, } -/// An opaque unique joint ID for a [`Component`] in an [`Archetype`] within a [`World`]. -/// -/// A component may be present within multiple archetypes, but each component within -/// each archetype has its own unique `ArchetypeComponentId`. This is leveraged by the system -/// schedulers to opportunistically run multiple systems in parallel that would otherwise -/// conflict. For example, `Query<&mut A, With>` and `Query<&mut A, Without>` can run in -/// parallel as the matched `ArchetypeComponentId` sets for both queries are disjoint, even -/// though `&mut A` on both queries point to the same [`ComponentId`]. -/// -/// In SQL terms, these IDs are composite keys on a [many-to-many relationship] between archetypes -/// and components. Each component type will have only one [`ComponentId`], but may have many -/// [`ArchetypeComponentId`]s, one for every archetype the component is present in. Likewise, each -/// archetype will have only one [`ArchetypeId`] but may have many [`ArchetypeComponentId`]s, one -/// for each component that belongs to the archetype. -/// -/// Every [`Resource`] is also assigned one of these IDs. As resources do not belong to any -/// particular archetype, a resource's ID uniquely identifies it. -/// -/// These IDs are only valid within a given World, and are not globally unique. -/// Attempting to use an ID on a world that it wasn't sourced from will -/// not point to the same archetype nor the same component. -/// -/// [`Component`]: crate::component::Component -/// [`World`]: crate::world::World -/// [`Resource`]: crate::resource::Resource -/// [many-to-many relationship]: https://en.wikipedia.org/wiki/Many-to-many_(data_model) -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct ArchetypeComponentId(usize); - -impl SparseSetIndex for ArchetypeComponentId { - #[inline] - fn sparse_set_index(&self) -> usize { - self.0 - } - - fn get_sparse_set_index(value: usize) -> Self { - Self(value) - } -} - /// Maps a [`ComponentId`] to the list of [`Archetypes`]([`Archetype`]) that contain the [`Component`](crate::component::Component), /// along with an [`ArchetypeRecord`] which contains some metadata about how the component is stored in the archetype. pub type ComponentIndex = HashMap>; @@ -794,7 +765,6 @@ pub type ComponentIndex = HashMap, - archetype_component_count: usize, /// find the archetype id by the archetype's components by_components: HashMap, /// find all the archetypes that contain a component @@ -818,7 +788,6 @@ impl Archetypes { archetypes: Vec::new(), by_components: Default::default(), by_component: Default::default(), - archetype_component_count: 0, }; // SAFETY: Empty archetype has no components unsafe { @@ -873,22 +842,6 @@ impl Archetypes { } } - /// Generate and store a new [`ArchetypeComponentId`]. - /// - /// This simply increment the counter and return the new value. - /// - /// # Panics - /// - /// On archetype component id overflow. - pub(crate) fn new_archetype_component_id(&mut self) -> ArchetypeComponentId { - let id = ArchetypeComponentId(self.archetype_component_count); - self.archetype_component_count = self - .archetype_component_count - .checked_add(1) - .expect("archetype_component_count overflow"); - id - } - /// Fetches an immutable reference to an [`Archetype`] using its /// ID. Returns `None` if no corresponding archetype exists. #[inline] @@ -921,6 +874,10 @@ impl Archetypes { } /// Gets the archetype id matching the given inputs or inserts a new one if it doesn't exist. + /// + /// Specifically, it returns a tuple where the first element + /// is the [`ArchetypeId`] that the given inputs belong to, and the second element is a boolean indicating whether a new archetype was created. + /// /// `table_components` and `sparse_set_components` must be sorted /// /// # Safety @@ -933,56 +890,35 @@ impl Archetypes { table_id: TableId, table_components: Vec, sparse_set_components: Vec, - ) -> ArchetypeId { + ) -> (ArchetypeId, bool) { let archetype_identity = ArchetypeComponents { sparse_set_components: sparse_set_components.into_boxed_slice(), table_components: table_components.into_boxed_slice(), }; let archetypes = &mut self.archetypes; - let archetype_component_count = &mut self.archetype_component_count; let component_index = &mut self.by_component; - *self - .by_components - .entry(archetype_identity) - .or_insert_with_key(move |identity| { + match self.by_components.entry(archetype_identity) { + Entry::Occupied(occupied) => (*occupied.get(), false), + Entry::Vacant(vacant) => { let ArchetypeComponents { table_components, sparse_set_components, - } = identity; + } = vacant.key(); let id = ArchetypeId::new(archetypes.len()); - let table_start = *archetype_component_count; - *archetype_component_count += table_components.len(); - let table_archetype_components = - (table_start..*archetype_component_count).map(ArchetypeComponentId); - let sparse_start = *archetype_component_count; - *archetype_component_count += sparse_set_components.len(); - let sparse_set_archetype_components = - (sparse_start..*archetype_component_count).map(ArchetypeComponentId); archetypes.push(Archetype::new( components, component_index, observers, id, table_id, - table_components - .iter() - .copied() - .zip(table_archetype_components), - sparse_set_components - .iter() - .copied() - .zip(sparse_set_archetype_components), + table_components.iter().copied(), + sparse_set_components.iter().copied(), )); - id - }) - } - - /// Returns the number of components that are stored in archetypes. - /// Note that if some component `T` is stored in more than one archetype, it will be counted once for each archetype it's present in. - #[inline] - pub fn archetype_components_len(&self) -> usize { - self.archetype_component_count + vacant.insert(id); + (id, true) + } + } } /// Clears all entities from all archetypes. @@ -1024,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/batching.rs b/crates/bevy_ecs/src/batching.rs index cdffbfa05d..ab9f2d582c 100644 --- a/crates/bevy_ecs/src/batching.rs +++ b/crates/bevy_ecs/src/batching.rs @@ -22,7 +22,7 @@ use core::ops::Range; /// [`EventReader::par_read`]: crate::event::EventReader::par_read #[derive(Clone, Debug)] pub struct BatchingStrategy { - /// The upper and lower limits for a batch of entities. + /// The upper and lower limits for a batch of items. /// /// Setting the bounds to the same value will result in a fixed /// batch size. diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index a7fb4f6fd4..75c2c89ff8 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2,12 +2,63 @@ //! //! This module contains the [`Bundle`] trait and some other helper types. +/// Derive the [`Bundle`] trait +/// +/// You can apply this derive macro to structs that are +/// composed of [`Component`]s or +/// other [`Bundle`]s. +/// +/// ## Attributes +/// +/// Sometimes parts of the Bundle should not be inserted. +/// Those can be marked with `#[bundle(ignore)]`, and they will be skipped. +/// In that case, the field needs to implement [`Default`] unless you also ignore +/// the [`BundleFromComponents`] implementation. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # +/// #[derive(Bundle)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// +/// #[bundle(ignore)] +/// creator: Option +/// } +/// ``` +/// +/// Some fields may be bundles that do not implement +/// [`BundleFromComponents`]. This happens for bundles that cannot be extracted. +/// For example with [`SpawnRelatedBundle`](bevy_ecs::spawn::SpawnRelatedBundle), see below for an +/// example usage. +/// In those cases you can either ignore it as above, +/// or you can opt out the whole Struct by marking it as ignored with +/// `#[bundle(ignore_from_components)]`. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle, ChildOf, Spawn}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # #[derive(Component)] +/// # struct Marker; +/// # +/// use bevy_ecs::spawn::SpawnRelatedBundle; +/// +/// #[derive(Bundle)] +/// #[bundle(ignore_from_components)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// related_spawner: SpawnRelatedBundle>, +/// } +/// ``` pub use bevy_ecs_macros::Bundle; use crate::{ archetype::{ - Archetype, ArchetypeAfterBundleInsert, ArchetypeId, Archetypes, BundleComponentStatus, - ComponentStatus, SpawnBundleStatus, + Archetype, ArchetypeAfterBundleInsert, ArchetypeCreated, ArchetypeId, Archetypes, + BundleComponentStatus, ComponentStatus, SpawnBundleStatus, }, change_detection::MaybeLocation, component::{ @@ -15,15 +66,13 @@ use crate::{ RequiredComponents, StorageType, Tick, }, entity::{Entities, Entity, EntityLocation}, + lifecycle::{ADD, INSERT, REMOVE, REPLACE}, observer::Observers, prelude::World, query::DebugCheckedUnwrap, relationship::RelationshipHookMode, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::{ - unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REMOVE, - ON_REPLACE, - }, + world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut}, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform::collections::{HashMap, HashSet}; @@ -158,12 +207,6 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { /// Gets this [`Bundle`]'s component ids. This will be [`None`] if the component has not been registered. fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)); - - /// Registers components that are required by the components in this [`Bundle`]. - fn register_required_components( - _components: &mut ComponentsRegistrator, - _required_components: &mut RequiredComponents, - ); } /// Creates a [`Bundle`] by taking it from internal storage. @@ -230,20 +273,6 @@ unsafe impl Bundle for C { ids(components.register_component::()); } - fn register_required_components( - components: &mut ComponentsRegistrator, - required_components: &mut RequiredComponents, - ) { - let component_id = components.register_component::(); - ::register_required_components( - component_id, - components, - required_components, - 0, - &mut Vec::new(), - ); - } - fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)) { ids(components.get_id(TypeId::of::())); } @@ -298,13 +327,6 @@ macro_rules! tuple_impl { fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)){ $(<$name as Bundle>::get_component_ids(components, ids);)* } - - fn register_required_components( - components: &mut ComponentsRegistrator, - required_components: &mut RequiredComponents, - ) { - $(<$name as Bundle>::register_required_components(components, required_components);)* - } } #[expect( @@ -501,10 +523,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 @@ -732,7 +753,7 @@ impl BundleInfo { } } - /// Inserts a bundle into the given archetype and returns the resulting archetype. + /// Inserts a bundle into the given archetype and returns the resulting archetype and whether a new archetype was created. /// This could be the same [`ArchetypeId`], in the event that inserting the given bundle /// does not result in an [`Archetype`] change. /// @@ -747,12 +768,12 @@ impl BundleInfo { components: &Components, observers: &Observers, archetype_id: ArchetypeId, - ) -> ArchetypeId { + ) -> (ArchetypeId, bool) { if let Some(archetype_after_insert_id) = archetypes[archetype_id] .edges() .get_archetype_after_bundle_insert(self.id) { - return archetype_after_insert_id; + return (archetype_after_insert_id, false); } let mut new_table_components = Vec::new(); let mut new_sparse_set_components = Vec::new(); @@ -806,7 +827,7 @@ impl BundleInfo { added, existing, ); - archetype_id + (archetype_id, false) } else { let table_id; let table_components; @@ -842,13 +863,14 @@ impl BundleInfo { }; }; // SAFETY: ids in self must be valid - let new_archetype_id = archetypes.get_id_or_insert( + let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert( components, observers, table_id, table_components, sparse_set_components, ); + // Add an edge from the old archetype to the new archetype. archetypes[archetype_id] .edges_mut() @@ -860,11 +882,11 @@ impl BundleInfo { added, existing, ); - new_archetype_id + (new_archetype_id, is_new_created) } } - /// Removes a bundle from the given archetype and returns the resulting archetype + /// Removes a bundle from the given archetype and returns the resulting archetype and whether a new archetype was created. /// (or `None` if the removal was invalid). /// This could be the same [`ArchetypeId`], in the event that removing the given bundle /// does not result in an [`Archetype`] change. @@ -887,7 +909,7 @@ impl BundleInfo { observers: &Observers, archetype_id: ArchetypeId, intersection: bool, - ) -> Option { + ) -> (Option, bool) { // Check the archetype graph to see if the bundle has been // removed from this archetype in the past. let archetype_after_remove_result = { @@ -898,9 +920,9 @@ impl BundleInfo { edges.get_archetype_after_bundle_take(self.id()) } }; - let result = if let Some(result) = archetype_after_remove_result { + let (result, is_new_created) = if let Some(result) = archetype_after_remove_result { // This bundle removal result is cached. Just return that! - result + (result, false) } else { let mut next_table_components; let mut next_sparse_set_components; @@ -925,7 +947,7 @@ impl BundleInfo { current_archetype .edges_mut() .cache_archetype_after_bundle_take(self.id(), None); - return None; + return (None, false); } } @@ -953,14 +975,14 @@ impl BundleInfo { }; } - let new_archetype_id = archetypes.get_id_or_insert( + let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert( components, observers, next_table_id, next_table_components, next_sparse_set_components, ); - Some(new_archetype_id) + (Some(new_archetype_id), is_new_created) }; let current_archetype = &mut archetypes[archetype_id]; // Cache the result in an edge. @@ -973,7 +995,7 @@ impl BundleInfo { .edges_mut() .cache_archetype_after_bundle_take(self.id(), result); } - result + (result, is_new_created) } } @@ -1036,14 +1058,15 @@ impl<'w> BundleInserter<'w> { // SAFETY: We will not make any accesses to the command queue, component or resource data of this world let bundle_info = world.bundles.get_unchecked(bundle_id); let bundle_id = bundle_info.id(); - let new_archetype_id = bundle_info.insert_bundle_into_archetype( + let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype( &mut world.archetypes, &mut world.storages, &world.components, &world.observers, archetype_id, ); - if new_archetype_id == archetype_id { + + let inserter = if new_archetype_id == archetype_id { let archetype = &mut world.archetypes[archetype_id]; // SAFETY: The edge is assured to be initialized when we called insert_bundle_into_archetype let archetype_after_insert = unsafe { @@ -1103,7 +1126,15 @@ impl<'w> BundleInserter<'w> { world: world.as_unsafe_world_cell(), } } + }; + + if is_new_created { + inserter + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); } + inserter } /// # Safety @@ -1132,8 +1163,8 @@ impl<'w> BundleInserter<'w> { if insert_mode == InsertMode::Replace { if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), archetype_after_insert.iter_existing(), caller, ); @@ -1193,16 +1224,16 @@ impl<'w> BundleInserter<'w> { unsafe { entities.get(swapped_entity).debug_checked_unwrap() }; entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, + }), ); } let new_location = new_archetype.allocate(entity, result.table_row); - entities.set(entity.index(), new_location); + entities.set(entity.index(), Some(new_location)); let after_effect = bundle_info.write_components( table, sparse_sets, @@ -1242,19 +1273,19 @@ impl<'w> BundleInserter<'w> { unsafe { entities.get(swapped_entity).debug_checked_unwrap() }; entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, + }), ); } // PERF: store "non bundle" components in edge, then just move those to avoid // redundant copies let move_result = table.move_to_superset_unchecked(result.table_row, new_table); let new_location = new_archetype.allocate(entity, move_result.new_row); - entities.set(entity.index(), new_location); + entities.set(entity.index(), Some(new_location)); // If an entity was moved into this entity's table spot, update its table row. if let Some(swapped_entity) = move_result.swapped_entity { @@ -1264,12 +1295,12 @@ impl<'w> BundleInserter<'w> { entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: swapped_location.archetype_row, table_id: swapped_location.table_id, table_row: result.table_row, - }, + }), ); if archetype.id() == swapped_location.archetype_id { @@ -1317,8 +1348,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, - entity, + ADD, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1335,8 +1366,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), archetype_after_insert.iter_inserted(), caller, ); @@ -1354,8 +1385,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1421,7 +1452,7 @@ impl<'w> BundleRemover<'w> { ) -> Option { let bundle_info = world.bundles.get_unchecked(bundle_id); // SAFETY: Caller ensures archetype and bundle ids are correct. - let new_archetype_id = unsafe { + let (new_archetype_id, is_new_created) = unsafe { bundle_info.remove_bundle_from_archetype( &mut world.archetypes, &mut world.storages, @@ -1429,11 +1460,14 @@ impl<'w> BundleRemover<'w> { &world.observers, archetype_id, !require_all, - )? + ) }; + let new_archetype_id = new_archetype_id?; + if new_archetype_id == archetype_id { return None; } + let (old_archetype, new_archetype) = world.archetypes.get_2_mut(archetype_id, new_archetype_id); @@ -1447,13 +1481,20 @@ impl<'w> BundleRemover<'w> { Some((old.into(), new.into())) }; - Some(Self { + let remover = Self { bundle_info: bundle_info.into(), new_archetype: new_archetype.into(), old_archetype: old_archetype.into(), old_and_new_table: tables, world: world.as_unsafe_world_cell(), - }) + }; + if is_new_created { + remover + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); + } + Some(remover) } /// This can be passed to [`remove`](Self::remove) as the `pre_remove` function if you don't want to do anything before removing. @@ -1498,8 +1539,8 @@ impl<'w> BundleRemover<'w> { }; if self.old_archetype.as_ref().has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1513,8 +1554,8 @@ impl<'w> BundleRemover<'w> { ); if self.old_archetype.as_ref().has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, - entity, + REMOVE, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1543,7 +1584,7 @@ impl<'w> BundleRemover<'w> { // Handle sparse set removes for component_id in self.bundle_info.as_ref().iter_explicit_components() { if self.old_archetype.as_ref().contains(component_id) { - world.removed_components.send(component_id, entity); + world.removed_components.write(component_id, entity); // Make sure to drop components stored in sparse sets. // Dense components are dropped later in `move_to_and_drop_missing_unchecked`. @@ -1573,12 +1614,12 @@ impl<'w> BundleRemover<'w> { world.entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, + }), ); } @@ -1614,12 +1655,12 @@ impl<'w> BundleRemover<'w> { world.entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: swapped_location.archetype_row, table_id: swapped_location.table_id, table_row: location.table_row, - }, + }), ); world.archetypes[swapped_location.archetype_id] .set_entity_table_row(swapped_location.archetype_row, location.table_row); @@ -1635,7 +1676,7 @@ impl<'w> BundleRemover<'w> { // SAFETY: The entity is valid and has been moved to the new location already. unsafe { - world.entities.set(entity.index(), new_location); + world.entities.set(entity.index(), Some(new_location)); } (new_location, pre_remove_result) @@ -1675,22 +1716,30 @@ impl<'w> BundleSpawner<'w> { change_tick: Tick, ) -> Self { let bundle_info = world.bundles.get_unchecked(bundle_id); - let new_archetype_id = bundle_info.insert_bundle_into_archetype( + let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype( &mut world.archetypes, &mut world.storages, &world.components, &world.observers, ArchetypeId::EMPTY, ); + let archetype = &mut world.archetypes[new_archetype_id]; let table = &mut world.storages.tables[archetype.table_id()]; - Self { + let spawner = Self { bundle_info: bundle_info.into(), table: table.into(), archetype: archetype.into(), change_tick, world: world.as_unsafe_world_cell(), + }; + if is_new_created { + spawner + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); } + spawner } #[inline] @@ -1736,7 +1785,8 @@ impl<'w> BundleSpawner<'w> { InsertMode::Replace, caller, ); - entities.set_spawn_despawn(entity.index(), location, caller, self.change_tick); + entities.set(entity.index(), Some(location)); + entities.mark_spawn_despawn(entity.index(), caller, self.change_tick); (location, after_effect) }; @@ -1755,8 +1805,8 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, - entity, + ADD, + Some(entity), bundle_info.iter_contributed_components(), caller, ); @@ -1770,8 +1820,8 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), bundle_info.iter_contributed_components(), caller, ); @@ -2042,7 +2092,9 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) { #[cfg(test)] mod tests { - use crate::{component::HookContext, prelude::*, world::DeferredWorld}; + use crate::{ + archetype::ArchetypeCreated, lifecycle::HookContext, prelude::*, world::DeferredWorld, + }; use alloc::vec; #[derive(Component)] @@ -2091,6 +2143,26 @@ mod tests { } } + #[derive(Bundle)] + #[bundle(ignore_from_components)] + struct BundleNoExtract { + b: B, + no_from_comp: crate::spawn::SpawnRelatedBundle>, + } + + #[test] + fn can_spawn_bundle_without_extract() { + let mut world = World::new(); + let id = world + .spawn(BundleNoExtract { + b: B, + no_from_comp: Children::spawn(Spawn(C)), + }) + .id(); + + assert!(world.entity(id).get::().is_some()); + } + #[test] fn component_hook_order_spawn_despawn() { let mut world = World::new(); @@ -2237,6 +2309,28 @@ mod tests { assert_eq!(entity.get(), Some(&V("one"))); } + #[derive(Component, Debug, Eq, PartialEq)] + #[component(storage = "SparseSet")] + pub struct SparseV(&'static str); + + #[derive(Component, Debug, Eq, PartialEq)] + #[component(storage = "SparseSet")] + pub struct SparseA; + + #[test] + fn sparse_set_insert_if_new() { + let mut world = World::new(); + let id = world.spawn(SparseV("one")).id(); + let mut entity = world.entity_mut(id); + entity.insert_if_new(SparseV("two")); + entity.insert_if_new((SparseA, SparseV("three"))); + entity.flush(); + // should still contain "one" + let entity = world.entity(id); + assert!(entity.contains::()); + assert_eq!(entity.get(), Some(&SparseV("one"))); + } + #[test] fn sorted_remove() { let mut a = vec![1, 2, 3, 4, 5, 6, 7]; @@ -2257,4 +2351,32 @@ mod tests { assert_eq!(a, vec![1]); } + + #[test] + fn new_archetype_created() { + let mut world = World::new(); + #[derive(Resource, Default)] + struct Count(u32); + world.init_resource::(); + world.add_observer(|_t: On, mut count: ResMut| { + count.0 += 1; + }); + + let mut e = world.spawn((A, B)); + e.insert(C); + e.remove::(); + e.insert(A); + e.insert(A); + + 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 fdb5d496c5..05b76207b7 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -188,10 +188,10 @@ pub trait DetectChangesMut: DetectChanges { /// # /// # // first time `reset_score` runs, the score is changed. /// # schedule.run(&mut world); - /// # assert!(score_changed.run((), &mut world)); + /// # assert!(score_changed.run((), &mut world).unwrap()); /// # // second time `reset_score` runs, the score is not changed. /// # schedule.run(&mut world); - /// # assert!(!score_changed.run((), &mut world)); + /// # assert!(!score_changed.run((), &mut world).unwrap()); /// ``` #[inline] #[track_caller] @@ -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, @@ -263,12 +263,12 @@ pub trait DetectChangesMut: DetectChanges { /// # /// # // first time `reset_score` runs, the score is changed. /// # schedule.run(&mut world); - /// # assert!(score_changed.run((), &mut world)); - /// # assert!(score_changed_event.run((), &mut world)); + /// # assert!(score_changed.run((), &mut world).unwrap()); + /// # assert!(score_changed_event.run((), &mut world).unwrap()); /// # // second time `reset_score` runs, the score is not changed. /// # schedule.run(&mut world); - /// # assert!(!score_changed.run((), &mut world)); - /// # assert!(!score_changed_event.run((), &mut world)); + /// # assert!(!score_changed.run((), &mut world).unwrap()); + /// # assert!(!score_changed_event.run((), &mut world).unwrap()); /// ``` #[inline] #[must_use = "If you don't need to handle the previous value, use `set_if_neq` instead."] @@ -315,10 +315,10 @@ pub trait DetectChangesMut: DetectChanges { /// # /// # // first time `reset_score` runs, the score is changed. /// # schedule.run(&mut world); - /// # assert!(message_changed.run((), &mut world)); + /// # assert!(message_changed.run((), &mut world).unwrap()); /// # // second time `reset_score` runs, the score is not changed. /// # schedule.run(&mut world); - /// # assert!(!message_changed.run((), &mut world)); + /// # assert!(!message_changed.run((), &mut world).unwrap()); /// ``` fn clone_from_if_neq(&mut self, value: &T) -> bool where @@ -898,63 +898,39 @@ impl_debug!(Ref<'w, T>,); /// Unique mutable borrow of an entity's component or of a resource. /// -/// This can be used in queries to opt into change detection on both their mutable and immutable forms, as opposed to -/// `&mut T`, which only provides access to change detection while in its mutable form: +/// This can be used in queries to access change detection from immutable query methods, as opposed +/// to `&mut T` which only provides access to change detection from mutable query methods. /// /// ```rust /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::query::QueryData; /// # -/// #[derive(Component, Clone)] +/// #[derive(Component, Clone, Debug)] /// struct Name(String); /// -/// #[derive(Component, Clone, Copy)] +/// #[derive(Component, Clone, Copy, Debug)] /// struct Health(f32); /// -/// #[derive(Component, Clone, Copy)] -/// struct Position { -/// x: f32, -/// y: f32, -/// }; +/// fn my_system(mut query: Query<(Mut, &mut Health)>) { +/// // Mutable access provides change detection information for both parameters: +/// // - `name` has type `Mut` +/// // - `health` has type `Mut` +/// for (name, health) in query.iter_mut() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?} (last changed: {:?})", health, health.last_changed()); +/// # println!("{}{}", name.0, health.0); // Silence dead_code warning +/// } /// -/// #[derive(Component, Clone, Copy)] -/// struct Player { -/// id: usize, -/// }; -/// -/// #[derive(QueryData)] -/// #[query_data(mutable)] -/// struct PlayerQuery { -/// id: &'static Player, -/// -/// // Reacting to `PlayerName` changes is expensive, so we need to enable change detection when reading it. -/// name: Mut<'static, Name>, -/// -/// health: &'static mut Health, -/// position: &'static mut Position, -/// } -/// -/// fn update_player_avatars(players_query: Query) { -/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`. -/// for player in players_query.iter() { -/// if player.name.is_changed() { -/// // Update the player's name. This clones a String, and so is more expensive. -/// update_player_name(player.id, player.name.clone()); -/// } -/// -/// // Update the health bar. -/// update_player_health(player.id, *player.health); -/// -/// // Update the player's position. -/// update_player_position(player.id, *player.position); +/// // Immutable access only provides change detection for `Name`: +/// // - `name` has type `Ref` +/// // - `health` has type `&Health` +/// for (name, health) in query.iter() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?}", health); /// } /// } /// -/// # bevy_ecs::system::assert_is_system(update_player_avatars); -/// -/// # fn update_player_name(player: &Player, new_name: Name) {} -/// # fn update_player_health(player: &Player, new_health: Health) {} -/// # fn update_player_position(player: &Player, new_position: Position) {} +/// # bevy_ecs::system::assert_is_system(my_system); /// ``` pub struct Mut<'w, T: ?Sized> { pub(crate) value: &'w mut T, @@ -1517,7 +1493,7 @@ impl MaybeLocation { /// within a non-tracked function body. #[inline] #[track_caller] - pub fn caller() -> Self { + pub const fn caller() -> Self { // Note that this cannot use `new_with`, since `FnOnce` invocations cannot be annotated with `#[track_caller]`. MaybeLocation { #[cfg(feature = "track_location")] @@ -1590,7 +1566,7 @@ mod tests { // world: 1, system last ran: 0, component changed: 1 // The spawn will be detected since it happened after the system "last ran". - assert!(change_detected_system.run((), &mut world)); + assert!(change_detected_system.run((), &mut world).unwrap()); // world: 1 + MAX_CHANGE_AGE let change_tick = world.change_tick.get_mut(); @@ -1600,7 +1576,7 @@ mod tests { // Since we clamp things to `MAX_CHANGE_AGE` for determinism, // `ComponentTicks::is_changed` will now see `MAX_CHANGE_AGE > MAX_CHANGE_AGE` // and return `false`. - assert!(!change_expired_system.run((), &mut world)); + assert!(!change_expired_system.run((), &mut world).unwrap()); } #[test] diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index e7c14d1c3e..615c5903f8 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -5,16 +5,17 @@ use crate::{ bundle::BundleInfo, change_detection::{MaybeLocation, MAX_CHANGE_AGE}, entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent}, + lifecycle::{ComponentHook, ComponentHooks}, query::DebugCheckedUnwrap, - relationship::RelationshipHookMode, resource::Resource, storage::{SparseSetIndex, SparseSets, Table, TableRow}, system::{Local, SystemParam}, - world::{DeferredWorld, FromWorld, World}, + world::{FromWorld, World}, }; use alloc::boxed::Box; use alloc::{borrow::Cow, format, vec::Vec}; pub use bevy_ecs_macros::Component; +use bevy_ecs_macros::Event; use bevy_platform::sync::Arc; use bevy_platform::{ collections::{HashMap, HashSet}, @@ -23,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}, @@ -33,7 +34,6 @@ use core::{ mem::needs_drop, ops::{Deref, DerefMut}, }; -use disqualified::ShortName; use smallvec::SmallVec; use thiserror::Error; @@ -375,7 +375,8 @@ use thiserror::Error; /// - `#[component(on_remove = on_remove_function)]` /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::component::ComponentId; @@ -404,7 +405,8 @@ use thiserror::Error; /// This also supports function calls that yield closures /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # /// #[derive(Component)] @@ -481,7 +483,7 @@ use thiserror::Error; /// ``` /// # use std::cell::RefCell; /// # use bevy_ecs::component::Component; -/// use bevy_utils::synccell::SyncCell; +/// use bevy_platform::cell::SyncCell; /// /// // This will compile. /// #[derive(Component)] @@ -490,7 +492,7 @@ use thiserror::Error; /// } /// ``` /// -/// [`SyncCell`]: bevy_utils::synccell::SyncCell +/// [`SyncCell`]: bevy_platform::cell::SyncCell /// [`Exclusive`]: https://doc.rust-lang.org/nightly/std/sync/struct.Exclusive.html #[diagnostic::on_unimplemented( message = "`{Self}` is not a `Component`", @@ -576,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) {} } @@ -595,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. @@ -621,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; } @@ -631,6 +693,7 @@ impl ComponentMutability for Immutable { pub struct Mutable; impl private::Seal for Mutable {} + impl ComponentMutability for Mutable { const MUTABLE: bool = true; } @@ -656,244 +719,6 @@ pub enum StorageType { SparseSet, } -/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. -pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); - -/// Context provided to a [`ComponentHook`]. -#[derive(Clone, Copy, Debug)] -pub struct HookContext { - /// The [`Entity`] this hook was invoked for. - pub entity: Entity, - /// The [`ComponentId`] this hook was invoked for. - pub component_id: ComponentId, - /// The caller location is `Some` if the `track_caller` feature is enabled. - pub caller: MaybeLocation, - /// Configures how relationship hooks will run - pub relationship_hook_mode: RelationshipHookMode, -} - -/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. -/// -/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. -/// These are intended to be used for structural side effects that need to happen when a component is added or removed, -/// and are not intended for general-purpose logic. -/// -/// For example, you might use a hook to update a cached index when a component is added, -/// to clean up resources when a component is removed, -/// or to keep hierarchical data structures across entities in sync. -/// -/// This information is stored in the [`ComponentInfo`] of the associated component. -/// -/// There is two ways of configuring hooks for a component: -/// 1. Defining the relevant hooks on the [`Component`] implementation -/// 2. Using the [`World::register_component_hooks`] method -/// -/// # Example 2 -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// use bevy_platform::collections::HashSet; -/// -/// #[derive(Component)] -/// struct MyTrackedComponent; -/// -/// #[derive(Resource, Default)] -/// struct TrackedEntities(HashSet); -/// -/// let mut world = World::new(); -/// world.init_resource::(); -/// -/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks -/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); -/// assert!(tracked_component_query.iter(&world).next().is_none()); -/// -/// world.register_component_hooks::().on_add(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.insert(context.entity); -/// }); -/// -/// world.register_component_hooks::().on_remove(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.remove(&context.entity); -/// }); -/// -/// let entity = world.spawn(MyTrackedComponent).id(); -/// let tracked_entities = world.resource::(); -/// assert!(tracked_entities.0.contains(&entity)); -/// -/// world.despawn(entity); -/// let tracked_entities = world.resource::(); -/// assert!(!tracked_entities.0.contains(&entity)); -/// ``` -#[derive(Debug, Clone, Default)] -pub struct ComponentHooks { - pub(crate) on_add: Option, - pub(crate) on_insert: Option, - pub(crate) on_replace: Option, - pub(crate) on_remove: Option, - pub(crate) on_despawn: Option, -} - -impl ComponentHooks { - pub(crate) fn update_from_component(&mut self) -> &mut Self { - if let Some(hook) = C::on_add() { - self.on_add(hook); - } - if let Some(hook) = C::on_insert() { - self.on_insert(hook); - } - if let Some(hook) = C::on_replace() { - self.on_replace(hook); - } - if let Some(hook) = C::on_remove() { - self.on_remove(hook); - } - if let Some(hook) = C::on_despawn() { - self.on_despawn(hook); - } - - self - } - - /// Register a [`ComponentHook`] that will be run when this component is added to an entity. - /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as - /// adding all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_add` hook - pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_add(hook) - .expect("Component already has an on_add hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// or replaced. - /// - /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_insert` hook - pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_insert(hook) - .expect("Component already has an on_insert hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, - /// such as being replaced (with `.insert`) or removed. - /// - /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, - /// allowing access to the previous data just before it is dropped. - /// This hook does *not* run if the entity did not already have this component. - /// - /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_replace` hook - pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_replace(hook) - .expect("Component already has an on_replace hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// Despawning an entity counts as removing all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_remove` hook - pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_remove(hook) - .expect("Component already has an on_remove hook") - } - - /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_despawn` hook - pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_despawn(hook) - .expect("Component already has an on_despawn hook") - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. - /// - /// This is a fallible version of [`Self::on_add`]. - /// - /// Returns `None` if the component already has an `on_add` hook. - pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_add.is_some() { - return None; - } - self.on_add = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// - /// This is a fallible version of [`Self::on_insert`]. - /// - /// Returns `None` if the component already has an `on_insert` hook. - pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_insert.is_some() { - return None; - } - self.on_insert = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed - /// - /// This is a fallible version of [`Self::on_replace`]. - /// - /// Returns `None` if the component already has an `on_replace` hook. - pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_replace.is_some() { - return None; - } - self.on_replace = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// - /// This is a fallible version of [`Self::on_remove`]. - /// - /// Returns `None` if the component already has an `on_remove` hook. - pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_remove.is_some() { - return None; - } - self.on_remove = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// This is a fallible version of [`Self::on_despawn`]. - /// - /// Returns `None` if the component already has an `on_despawn` hook. - pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_despawn.is_some() { - return None; - } - self.on_despawn = Some(hook); - Some(self) - } -} - /// Stores metadata for a type of component or resource stored in a specific [`World`]. #[derive(Debug, Clone)] pub struct ComponentInfo { @@ -913,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. @@ -1071,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, @@ -1117,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::()), @@ -1142,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, @@ -1158,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, @@ -1173,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::()), @@ -1199,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. @@ -2052,7 +1877,7 @@ impl Components { } /// Gets the metadata associated with the given component, if it is registered. - /// This will return `None` if the id is not regiserted or is queued. + /// This will return `None` if the id is not registered or is queued. /// /// 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] @@ -2089,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 @@ -2400,7 +2222,7 @@ impl Components { /// * [`World::component_id()`] #[inline] pub fn valid_component_id(&self) -> Option { - self.get_id(TypeId::of::()) + self.get_valid_id(TypeId::of::()) } /// Type-erased equivalent of [`Components::valid_resource_id()`]. @@ -2431,7 +2253,7 @@ impl Components { /// * [`Components::get_resource_id()`] #[inline] pub fn valid_resource_id(&self) -> Option { - self.get_resource_id(TypeId::of::()) + self.get_valid_resource_id(TypeId::of::()) } /// Type-erased equivalent of [`Components::component_id()`]. @@ -2616,12 +2438,12 @@ impl Tick { /// /// Returns `true` if wrapping was performed. Otherwise, returns `false`. #[inline] - pub(crate) 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 @@ -2629,6 +2451,41 @@ impl Tick { } } +/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make +/// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus +/// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old. +/// +/// # Example +/// +/// Here a schedule is stored in a custom resource. This way the systems in it would not have their change +/// ticks automatically updated via [`World::check_change_ticks`], possibly causing `Tick`-related bugs on +/// long-running apps. +/// +/// To fix that, add an observer for this event that calls the schedule's +/// [`Schedule::check_change_ticks`](bevy_ecs::schedule::Schedule::check_change_ticks). +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_ecs::component::CheckChangeTicks; +/// +/// #[derive(Resource)] +/// struct CustomSchedule(Schedule); +/// +/// # let mut world = World::new(); +/// 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 present `Tick` that other ticks get compared to. + pub fn present_tick(self) -> Tick { + self.0 + } +} + /// Interior-mutable access to the [`Tick`]s for a single component or resource. #[derive(Copy, Clone, Debug)] pub struct TickCells<'a> { @@ -3013,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() @@ -3172,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 @@ -3183,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 5328eb1d3a..4f0f082c50 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -1,11 +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::{ - bundle::Bundle, + archetype::Archetype, + bundle::{Bundle, BundleId, InsertMode}, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, query::DebugCheckedUnwrap, @@ -79,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>, @@ -103,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 ()>, @@ -118,7 +121,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { entities, mapper, component_info, - entity_cloner, + state: entity_cloner, type_registry, } } @@ -153,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`]. @@ -170,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") } @@ -269,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. @@ -278,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`]. @@ -305,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)); ///``` @@ -337,30 +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, - 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(), - 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. @@ -428,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(), } } @@ -441,110 +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"); - - #[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) { - 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. @@ -576,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 @@ -589,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; } @@ -599,48 +526,170 @@ impl EntityCloner { target } - fn is_cloning_allowed(&self, component: &ComponentId) -> bool { - (self.filter_allows_components && self.filter.contains(component)) - || (!self.filter_allows_components && !self.filter.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 } @@ -652,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_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_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 } @@ -743,8 +713,8 @@ impl<'w> EntityClonerBuilder<'w> { &mut self, clone_behavior: ComponentCloneBehavior, ) -> &mut Self { - if let Some(id) = self.world.components().component_id::() { - self.entity_cloner + if let Some(id) = self.world.components().valid_component_id::() { + self.state .clone_behavior_overrides .insert(id, clone_behavior); } @@ -760,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 @@ -768,8 +738,8 @@ 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().component_id::() { - self.entity_cloner.clone_behavior_overrides.remove(&id); + if let Some(id) = self.world.components().valid_component_id::() { + self.state.clone_behavior_overrides.remove(&id); } self } @@ -779,53 +749,265 @@ 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); - } - 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.insert(required_id); - } else { - self.entity_cloner.filter.remove(&required_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_ids(bundle_id) + } + + /// Extends the list of components that shouldn't be cloned. + /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these. + /// + /// 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 FilterableIds) -> &mut Self { + ids.filter_ids(&mut |ids| match ids { + FilterableId::Type(type_id) => { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter.filter_deny(id, self.world); + } + } + FilterableId::Component(component_id) => { + self.filter.filter_deny(component_id, self.world); + } + FilterableId::Bundle(bundle_id) => { + 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); } } } - } + }); + 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 } - /// 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); + /// 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_ids(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_ids_if_new(bundle_id) + } + + /// Extends the list of components to clone. + /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these. + /// + /// 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 FilterableIds) -> &mut Self { + self.allow_by_ids_inner(ids, InsertMode::Replace); + self + } + + /// Extends the list of components to clone if the target does not contain them. + /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these. + /// + /// 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 FilterableIds) -> &mut Self { + self.allow_by_ids_inner(ids, InsertMode::Keep); + self + } + + fn allow_by_ids_inner( + &mut self, + ids: impl FilterableIds, + insert_mode: InsertMode, + ) { + ids.filter_ids(&mut |id| match id { + FilterableId::Type(type_id) => { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter.filter_allow(id, self.world, insert_mode); + } + } + FilterableId::Component(component_id) => { + self.filter + .filter_allow(component_id, self.world, insert_mode); + } + FilterableId::Bundle(bundle_id) => { + 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, insert_mode); + } + } + } + }); + } +} + +/// 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); + } } - 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.remove(&required_id); - } else { - self.entity_cloner.filter.insert(required_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, + } + } +} + +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); } } } @@ -833,30 +1015,335 @@ 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; + } +} + +mod private { + use super::*; + + /// Marker trait to allow multiple blanket implementations for [`FilterableIds`]. + pub trait Marker {} + /// Marker struct for [`FilterableIds`] implementation for single-value types. + pub struct ScalarType {} + impl Marker for ScalarType {} + /// Marker struct for [`FilterableIds`] implementation for [`IntoIterator`] types. + pub struct VectorType {} + impl Marker for VectorType {} + + /// Defines types of ids that [`EntityClonerBuilder`] can filter components by. + #[derive(From)] + pub enum FilterableId { + Type(TypeId), + Component(ComponentId), + Bundle(BundleId), + } + + impl<'a, T> From<&'a T> for FilterableId + where + T: Into + Copy, + { + #[inline] + fn from(value: &'a T) -> Self { + (*value).into() + } + } + + /// A trait to allow [`EntityClonerBuilder`] filter by any supported id type and their iterators, + /// reducing the number of method permutations required for all id types. + /// + /// The supported id types that can be used to filter components are defined by [`FilterableId`], which allows following types: [`TypeId`], [`ComponentId`] and [`BundleId`]. + /// + /// `M` is a generic marker to allow multiple blanket implementations of this trait. + /// This works because `FilterableId` is a different trait from `FilterableId`, so multiple blanket implementations for different `M` are allowed. + /// The reason this is required is because supporting `IntoIterator` requires blanket implementation, but that will conflict with implementation for `TypeId` + /// since `IntoIterator` can technically be implemented for `TypeId` in the future. + /// Functions like `allow_by_ids` rely on type inference to automatically select proper type for `M` at call site. + pub trait FilterableIds { + /// Takes in a function that processes all types of [`FilterableId`] one-by-one. + fn filter_ids(self, ids: &mut impl FnMut(FilterableId)); + } + + impl FilterableIds for I + where + I: IntoIterator, + T: Into, + { + #[inline] + fn filter_ids(self, ids: &mut impl FnMut(FilterableId)) { + for id in self.into_iter() { + ids(id.into()); + } + } + } + + impl FilterableIds for T + where + T: Into, + { + #[inline] + fn filter_ids(self, ids: &mut impl FnMut(FilterableId)) { + ids(self.into()); + } + } +} + +use private::{FilterableId, FilterableIds, Marker}; + #[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}; @@ -879,7 +1366,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); @@ -964,7 +1451,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()) @@ -997,7 +1484,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()); } @@ -1005,7 +1492,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); } @@ -1034,7 +1521,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) @@ -1058,7 +1545,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); @@ -1072,10 +1559,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] @@ -1092,7 +1619,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)); } @@ -1114,8 +1641,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); @@ -1131,9 +1657,10 @@ mod tests { } #[derive(Component, Clone)] + #[require(C)] struct B; - #[derive(Component, Clone)] + #[derive(Component, Clone, Default)] struct C; let mut world = World::default(); @@ -1143,72 +1670,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)); @@ -1216,6 +1679,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)] @@ -1234,8 +1862,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); @@ -1262,8 +1889,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::(); }) @@ -1274,6 +1900,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; @@ -1314,7 +1994,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(); @@ -1336,7 +2016,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); @@ -1362,42 +2042,24 @@ 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<()>, - ); + fn cloning_with_required_components_preserves_existing() { + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(B(5))] + struct A; - #[derive(Resource)] - struct FromWorldCalled(bool); + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u32); - 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 mut world = World::default(); - 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 e = world.spawn((A, B(0))).id(); + let e_clone = world.spawn(B(1)).id(); - 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); + 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))); } } diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs index 3dac2fa749..2c59655275 100644 --- a/crates/bevy_ecs/src/entity/map_entities.rs +++ b/crates/bevy_ecs/src/entity/map_entities.rs @@ -1,5 +1,5 @@ pub use bevy_ecs_macros::MapEntities; -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use crate::{ entity::{hash_map::EntityHashMap, Entity}, @@ -7,11 +7,14 @@ use crate::{ }; use alloc::{ - collections::{BTreeSet, VecDeque}, + collections::{BTreeMap, BTreeSet, VecDeque}, vec::Vec, }; -use bevy_platform::collections::HashSet; -use core::{hash::BuildHasher, mem}; +use bevy_platform::collections::{HashMap, HashSet}; +use core::{ + hash::{BuildHasher, Hash}, + mem, +}; use smallvec::SmallVec; use super::EntityIndexSet; @@ -72,9 +75,22 @@ impl MapEntities for Option { } } -impl MapEntities - for HashSet +impl MapEntities + for HashMap { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = self + .drain() + .map(|(mut key_entities, mut value_entities)| { + key_entities.map_entities(entity_mapper); + value_entities.map_entities(entity_mapper); + (key_entities, value_entities) + }) + .collect(); + } +} + +impl MapEntities for HashSet { fn map_entities(&mut self, entity_mapper: &mut E) { *self = self .drain() @@ -86,9 +102,22 @@ impl MapEntiti } } -impl MapEntities - for IndexSet +impl MapEntities + for IndexMap { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = self + .drain(..) + .map(|(mut key_entities, mut value_entities)| { + key_entities.map_entities(entity_mapper); + value_entities.map_entities(entity_mapper); + (key_entities, value_entities) + }) + .collect(); + } +} + +impl MapEntities for IndexSet { fn map_entities(&mut self, entity_mapper: &mut E) { *self = self .drain(..) @@ -109,6 +138,19 @@ impl MapEntities for EntityIndexSet { } } +impl MapEntities for BTreeMap { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = mem::take(self) + .into_iter() + .map(|(mut key_entities, mut value_entities)| { + key_entities.map_entities(entity_mapper); + value_entities.map_entities(entity_mapper); + (key_entities, value_entities) + }) + .collect(); + } +} + impl MapEntities for BTreeSet { fn map_entities(&mut self, entity_mapper: &mut E) { *self = mem::take(self) @@ -121,6 +163,14 @@ impl MapEntities for BTreeSet { } } +impl MapEntities for [T; N] { + fn map_entities(&mut self, entity_mapper: &mut E) { + for entities in self.iter_mut() { + entities.map_entities(entity_mapper); + } + } +} + impl MapEntities for Vec { fn map_entities(&mut self, entity_mapper: &mut E) { for entities in self.iter_mut() { @@ -358,7 +408,10 @@ mod tests { // Next allocated entity should be a further generation on the same index let entity = world.spawn_empty().id(); assert_eq!(entity.index(), dead_ref.index()); - assert!(entity.generation() > dead_ref.generation()); + assert!(entity + .generation() + .cmp_approx(&dead_ref.generation()) + .is_gt()); } #[test] @@ -373,7 +426,10 @@ mod tests { // Next allocated entity should be a further generation on the same index let entity = world.spawn_empty().id(); assert_eq!(entity.index(), dead_ref.index()); - assert!(entity.generation() > dead_ref.generation()); + assert!(entity + .generation() + .cmp_approx(&dead_ref.generation()) + .is_gt()); } #[test] diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index e4d4b26d97..700a4e517f 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -76,18 +76,12 @@ 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; use bevy_platform::sync::atomic::Ordering; -use core::{ - fmt, - hash::Hash, - mem::{self, MaybeUninit}, - num::NonZero, - panic::Location, -}; +use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; use log::warn; #[cfg(feature = "serialize")] @@ -189,8 +183,25 @@ impl SparseSetIndex for EntityRow { /// This tracks different versions or generations of an [`EntityRow`]. /// Importantly, this can wrap, meaning each generation is not necessarily unique per [`EntityRow`]. /// -/// This should be treated as a opaque identifier, and it's internal representation may be subject to change. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display)] +/// This should be treated as a opaque identifier, and its internal representation may be subject to change. +/// +/// # Aliasing +/// +/// Internally [`EntityGeneration`] wraps a `u32`, so it can't represent *every* possible generation. +/// Eventually, generations can (and do) wrap or alias. +/// This can cause [`Entity`] and [`EntityGeneration`] values to be equal while still referring to different conceptual entities. +/// This can cause some surprising behavior: +/// +/// ``` +/// # use bevy_ecs::entity::EntityGeneration; +/// let (aliased, did_alias) = EntityGeneration::FIRST.after_versions(1u32 << 31).after_versions_and_could_alias(1u32 << 31); +/// assert!(did_alias); +/// assert!(EntityGeneration::FIRST == aliased); +/// ``` +/// +/// This can cause some unintended side effects. +/// See [`Entity`] docs for practical concerns and how to minimize any risks. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Display)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(opaque))] #[cfg_attr(feature = "bevy_reflect", reflect(Hash, PartialEq, Debug, Clone))] @@ -201,6 +212,9 @@ impl EntityGeneration { /// Represents the first generation of an [`EntityRow`]. pub const FIRST: Self = Self(0); + /// Non-wrapping difference between two generations after which a signed interpretation becomes negative. + const DIFF_MAX: u32 = 1u32 << 31; + /// Gets some bits that represent this value. /// The bits are opaque and should not be regarded as meaningful. #[inline(always)] @@ -232,11 +246,54 @@ impl EntityGeneration { let raw = self.0.overflowing_add(versions); (Self(raw.0), raw.1) } + + /// Compares two generations. + /// + /// Generations that are later will be [`Greater`](core::cmp::Ordering::Greater) than earlier ones. + /// + /// ``` + /// # use bevy_ecs::entity::EntityGeneration; + /// # use core::cmp::Ordering; + /// let later_generation = EntityGeneration::FIRST.after_versions(400); + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&later_generation), Ordering::Less); + /// + /// let (aliased, did_alias) = EntityGeneration::FIRST.after_versions(400).after_versions_and_could_alias(u32::MAX); + /// assert!(did_alias); + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&aliased), Ordering::Less); + /// ``` + /// + /// Ordering will be incorrect and [non-transitive](https://en.wikipedia.org/wiki/Transitive_relation) + /// for distant generations: + /// + /// ```should_panic + /// # use bevy_ecs::entity::EntityGeneration; + /// # use core::cmp::Ordering; + /// let later_generation = EntityGeneration::FIRST.after_versions(3u32 << 31); + /// let much_later_generation = later_generation.after_versions(3u32 << 31); + /// + /// // while these orderings are correct and pass assertions... + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&later_generation), Ordering::Less); + /// assert_eq!(later_generation.cmp_approx(&much_later_generation), Ordering::Less); + /// + /// // ... this ordering is not and the assertion fails! + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&much_later_generation), Ordering::Less); + /// ``` + /// + /// Because of this, `EntityGeneration` does not implement `Ord`/`PartialOrd`. + #[inline] + pub const fn cmp_approx(&self, other: &Self) -> core::cmp::Ordering { + use core::cmp::Ordering; + match self.0.wrapping_sub(other.0) { + 0 => Ordering::Equal, + 1..Self::DIFF_MAX => Ordering::Greater, + _ => Ordering::Less, + } + } } /// Lightweight identifier of an [entity](crate::entity). /// -/// The identifier is implemented using a [generational index]: a combination of an index and a generation. +/// The identifier is implemented using a [generational index]: a combination of an index ([`EntityRow`]) and a generation ([`EntityGeneration`]). /// This allows fast insertion after data removal in an array while minimizing loss of spatial locality. /// /// These identifiers are only valid on the [`World`] it's sourced from. Attempting to use an `Entity` to @@ -244,6 +301,19 @@ impl EntityGeneration { /// /// [generational index]: https://lucassardois.medium.com/generational-indices-guide-8e3c5f7fd594 /// +/// # Aliasing +/// +/// Once an entity is despawned, it ceases to exist. +/// However, its [`Entity`] id is still present, and may still be contained in some data. +/// This becomes problematic because it is possible for a later entity to be spawned at the exact same id! +/// If this happens, which is rare but very possible, it will be logged. +/// +/// Aliasing can happen without warning. +/// Holding onto a [`Entity`] id corresponding to an entity well after that entity was despawned can cause un-intuitive behavior for both ordering, and comparing in general. +/// To prevent these bugs, it is generally best practice to stop holding an [`Entity`] or [`EntityGeneration`] value as soon as you know it has been despawned. +/// If you must do otherwise, do not assume the [`Entity`] corresponds to the same conceptual entity it originally did. +/// See [`EntityGeneration`]'s docs for more information about aliasing and why it occurs. +/// /// # Stability warning /// For all intents and purposes, `Entity` should be treated as an opaque identifier. The internal bit /// representation is liable to change from release to release as are the behaviors or performance @@ -648,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. @@ -826,8 +897,10 @@ impl Entities { /// Destroy an entity, allowing it to be reused. /// + /// Returns the `Option` of the entity or `None` if the `entity` was not present. + /// /// Must not be called while reserved entities are awaiting `flush()`. - pub fn free(&mut self, entity: Entity) -> Option { + pub fn free(&mut self, entity: Entity) -> Option { self.verify_flushed(); let meta = &mut self.meta[entity.index() as usize]; @@ -889,58 +962,46 @@ impl Entities { *self.free_cursor.get_mut() = 0; } - /// Returns the location of an [`Entity`]. - /// Note: for pending entities, returns `None`. + /// Returns the [`EntityLocation`] of an [`Entity`]. + /// Note: for pending entities and entities not participating in the ECS (entities with a [`EntityIdLocation`] of `None`), returns `None`. #[inline] pub fn get(&self, entity: Entity) -> Option { - if let Some(meta) = self.meta.get(entity.index() as usize) { - if meta.generation != entity.generation - || meta.location.archetype_id == ArchetypeId::INVALID - { - return None; - } - Some(meta.location) - } else { - None - } + self.get_id_location(entity).flatten() } - /// Updates the location of an [`Entity`]. This must be called when moving the components of - /// the existing entity around in storage. - /// - /// For spawning and despawning entities, [`set_spawn_despawn`](Self::set_spawn_despawn) must - /// be used instead. + /// Returns the [`EntityIdLocation`] of an [`Entity`]. + /// Note: for pending entities, returns `None`. + #[inline] + pub fn get_id_location(&self, entity: Entity) -> Option { + self.meta + .get(entity.index() as usize) + .filter(|meta| meta.generation == entity.generation) + .map(|meta| meta.location) + } + + /// Updates the location of an [`Entity`]. + /// This must be called when moving the components of the existing entity around in storage. /// /// # Safety /// - `index` must be a valid entity index. /// - `location` must be valid for the entity at `index` or immediately made valid afterwards /// before handing control to unknown code. #[inline] - pub(crate) unsafe fn set(&mut self, index: u32, location: EntityLocation) { + pub(crate) unsafe fn set(&mut self, index: u32, location: EntityIdLocation) { // SAFETY: Caller guarantees that `index` a valid entity index let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; meta.location = location; } - /// Updates the location of an [`Entity`]. This must be called when moving the components of - /// the spawned or despawned entity around in storage. + /// Mark an [`Entity`] as spawned or despawned in the given tick. /// /// # Safety /// - `index` must be a valid entity index. - /// - `location` must be valid for the entity at `index` or immediately made valid afterwards - /// before handing control to unknown code. #[inline] - pub(crate) unsafe fn set_spawn_despawn( - &mut self, - index: u32, - location: EntityLocation, - by: MaybeLocation, - at: Tick, - ) { + pub(crate) unsafe fn mark_spawn_despawn(&mut self, index: u32, by: MaybeLocation, at: Tick) { // SAFETY: Caller guarantees that `index` a valid entity index let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; - meta.location = location; - meta.spawned_or_despawned = MaybeUninit::new(SpawnedOrDespawned { by, at }); + meta.spawned_or_despawned = SpawnedOrDespawned { by, at }; } /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this @@ -954,7 +1015,7 @@ impl Entities { } let meta = &mut self.meta[index as usize]; - if meta.location.archetype_id == ArchetypeId::INVALID { + if meta.location.is_none() { meta.generation = meta.generation.after_versions(generations); true } else { @@ -989,6 +1050,8 @@ impl Entities { /// Allocates space for entities previously reserved with [`reserve_entity`](Entities::reserve_entity) or /// [`reserve_entities`](Entities::reserve_entities), then initializes each one using the supplied function. /// + /// See [`EntityLocation`] for details on its meaning and how to set it. + /// /// # Safety /// Flush _must_ set the entity location to the correct [`ArchetypeId`] for the given [`Entity`] /// each time init is called. This _can_ be [`ArchetypeId::INVALID`], provided the [`Entity`] @@ -996,7 +1059,12 @@ impl Entities { /// /// Note: freshly-allocated entities (ones which don't come from the pending list) are guaranteed /// to be initialized with the invalid archetype. - pub unsafe fn flush(&mut self, mut init: impl FnMut(Entity, &mut EntityLocation)) { + pub unsafe fn flush( + &mut self, + mut init: impl FnMut(Entity, &mut EntityIdLocation), + by: MaybeLocation, + at: Tick, + ) { let free_cursor = self.free_cursor.get_mut(); let current_free_cursor = *free_cursor; @@ -1013,6 +1081,7 @@ impl Entities { Entity::from_raw_and_generation(row, meta.generation), &mut meta.location, ); + meta.spawned_or_despawned = SpawnedOrDespawned { by, at }; } *free_cursor = 0; @@ -1025,18 +1094,23 @@ impl Entities { Entity::from_raw_and_generation(row, meta.generation), &mut meta.location, ); + meta.spawned_or_despawned = SpawnedOrDespawned { by, at }; } } /// Flushes all reserved entities to an "invalid" state. Attempting to retrieve them will return `None` /// unless they are later populated with a valid archetype. - pub fn flush_as_invalid(&mut self) { + pub fn flush_as_invalid(&mut self, by: MaybeLocation, at: Tick) { // SAFETY: as per `flush` safety docs, the archetype id can be set to [`ArchetypeId::INVALID`] if // the [`Entity`] has not been assigned to an [`Archetype`][crate::archetype::Archetype], which is the case here unsafe { - self.flush(|_entity, location| { - location.archetype_id = ArchetypeId::INVALID; - }); + self.flush( + |_entity, location| { + *location = None; + }, + by, + at, + ); } } @@ -1083,8 +1157,10 @@ impl Entities { self.len() == 0 } - /// Returns the source code location from which this entity has last been spawned - /// or despawned. Returns `None` if its index has been reused by another entity + /// Try to get the source code location from which this entity has last been + /// spawned, despawned or flushed. + /// + /// Returns `None` if its index has been reused by another entity /// or if this entity has never existed. pub fn entity_get_spawned_or_despawned_by( &self, @@ -1096,17 +1172,21 @@ impl Entities { }) } - /// Returns the [`Tick`] at which this entity has last been spawned or despawned. + /// Try to get the [`Tick`] at which this entity has last been + /// spawned, despawned or flushed. + /// /// Returns `None` if its index has been reused by another entity or if this entity - /// has never existed. + /// has never been spawned. pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option { self.entity_get_spawned_or_despawned(entity) .map(|spawned_or_despawned| spawned_or_despawned.at) } - /// Returns the [`SpawnedOrDespawned`] related to the entity's last spawn or - /// respawn. Returns `None` if its index has been reused by another entity or if - /// this entity has never existed. + /// Try to get the [`SpawnedOrDespawned`] related to the entity's last spawn, + /// despawn or flush. + /// + /// Returns `None` if its index has been reused by another entity or if + /// this entity has never been spawned. #[inline] fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { self.meta @@ -1114,12 +1194,9 @@ impl Entities { .filter(|meta| // Generation is incremented immediately upon despawn (meta.generation == entity.generation) - || (meta.location.archetype_id == ArchetypeId::INVALID) + || meta.location.is_none() && (meta.generation == entity.generation.after_versions(1))) - .map(|meta| { - // SAFETY: valid archetype or non-min generation is proof this is init - unsafe { meta.spawned_or_despawned.assume_init() } - }) + .map(|meta| meta.spawned_or_despawned) } /// Returns the source code location from which this entity has last been spawned @@ -1136,21 +1213,13 @@ impl Entities { ) -> (MaybeLocation, Tick) { // SAFETY: caller ensures entity is allocated let meta = unsafe { self.meta.get_unchecked(entity.index() as usize) }; - // SAFETY: caller ensures entities of this index were at least spawned - let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init() }; - (spawned_or_despawned.by, spawned_or_despawned.at) + (meta.spawned_or_despawned.by, meta.spawned_or_despawned.at) } #[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 { - if meta.generation != EntityGeneration::FIRST - || meta.location.archetype_id != ArchetypeId::INVALID - { - // SAFETY: non-min generation or valid archetype is proof this is init - let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init_mut() }; - spawned_or_despawned.at.check_tick(change_tick); - } + meta.spawned_or_despawned.at.check_tick(check); } } @@ -1210,11 +1279,11 @@ impl fmt::Display for EntityDoesNotExistDetails { #[derive(Copy, Clone, Debug)] struct EntityMeta { /// The current [`EntityGeneration`] of the [`EntityRow`]. - pub generation: EntityGeneration, - /// The current location of the [`EntityRow`] - pub location: EntityLocation, - /// Location of the last spawn or despawn of this entity - spawned_or_despawned: MaybeUninit, + generation: EntityGeneration, + /// The current location of the [`EntityRow`]. + location: EntityIdLocation, + /// Location and tick of the last spawn, despawn or flush of this entity. + spawned_or_despawned: SpawnedOrDespawned, } #[derive(Copy, Clone, Debug)] @@ -1227,8 +1296,11 @@ impl EntityMeta { /// meta for **pending entity** const EMPTY: EntityMeta = EntityMeta { generation: EntityGeneration::FIRST, - location: EntityLocation::INVALID, - spawned_or_despawned: MaybeUninit::uninit(), + location: None, + spawned_or_despawned: SpawnedOrDespawned { + by: MaybeLocation::caller(), + at: Tick::new(0), + }, }; } @@ -1256,15 +1328,15 @@ pub struct EntityLocation { pub table_row: TableRow, } -impl EntityLocation { - /// location for **pending entity** and **invalid entity** - pub(crate) const INVALID: EntityLocation = EntityLocation { - archetype_id: ArchetypeId::INVALID, - archetype_row: ArchetypeRow::INVALID, - table_id: TableId::INVALID, - table_row: TableRow::INVALID, - }; -} +/// An [`Entity`] id may or may not correspond to a valid conceptual entity. +/// If it does, the conceptual entity may or may not have a location. +/// If it has no location, the [`EntityLocation`] will be `None`. +/// An location of `None` means the entity effectively does not exist; it has an id, but is not participating in the ECS. +/// This is different from a location in the empty archetype, which is participating (queryable, etc) but just happens to have no components. +/// +/// Setting a location to `None` is often helpful when you want to destruct an entity or yank it from the ECS without allowing another system to reuse the id for something else. +/// It is also useful for reserving an id; commands will often allocate an `Entity` but not provide it a location until the command is applied. +pub type EntityIdLocation = Option; #[cfg(test)] mod tests { @@ -1294,7 +1366,7 @@ mod tests { let mut e = Entities::new(); e.reserve_entity(); // SAFETY: entity_location is left invalid - unsafe { e.flush(|_, _| {}) }; + unsafe { e.flush(|_, _| {}, MaybeLocation::caller(), Tick::default()) }; assert_eq!(e.len(), 1); } @@ -1307,9 +1379,13 @@ mod tests { // SAFETY: entity_location is left invalid unsafe { - entities.flush(|_entity, _location| { - // do nothing ... leaving entity location invalid - }); + entities.flush( + |_entity, _location| { + // do nothing ... leaving entity location invalid + }, + MaybeLocation::caller(), + Tick::default(), + ); }; assert!(entities.contains(e)); @@ -1357,7 +1433,10 @@ mod tests { // The very next entity allocated should be a further generation on the same index let next_entity = entities.alloc(); assert_eq!(next_entity.index(), entity.index()); - assert!(next_entity.generation() > entity.generation().after_versions(GENERATIONS)); + assert!(next_entity + .generation() + .cmp_approx(&entity.generation().after_versions(GENERATIONS)) + .is_gt()); } #[test] @@ -1544,25 +1623,43 @@ mod tests { } } + #[test] + fn entity_generation_is_approximately_ordered() { + use core::cmp::Ordering; + + let old = EntityGeneration::FIRST; + let middle = old.after_versions(1); + let younger_before_ord_wrap = middle.after_versions(EntityGeneration::DIFF_MAX); + let younger_after_ord_wrap = younger_before_ord_wrap.after_versions(1); + + assert_eq!(middle.cmp_approx(&old), Ordering::Greater); + assert_eq!(middle.cmp_approx(&middle), Ordering::Equal); + assert_eq!(middle.cmp_approx(&younger_before_ord_wrap), Ordering::Less); + assert_eq!( + middle.cmp_approx(&younger_after_ord_wrap), + Ordering::Greater + ); + } + #[test] fn entity_debug() { let entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap())); - let string = format!("{:?}", entity); + let string = format!("{entity:?}"); assert_eq!(string, "42v0#4294967253"); let entity = Entity::PLACEHOLDER; - let string = format!("{:?}", entity); + let string = format!("{entity:?}"); assert_eq!(string, "PLACEHOLDER"); } #[test] fn entity_display() { let entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap())); - let string = format!("{}", entity); + let string = format!("{entity}"); assert_eq!(string, "42v0"); let entity = Entity::PLACEHOLDER; - let string = format!("{}", entity); + let string = format!("{entity}"); assert_eq!(string, "PLACEHOLDER"); } } 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/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 0686e68f1d..c290e249b2 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -76,7 +76,7 @@ impl BevyError { break; } } - writeln!(f, "{}", line)?; + writeln!(f, "{line}")?; } if !full_backtrace { if std::thread::panicking() { diff --git a/crates/bevy_ecs/src/error/command_handling.rs b/crates/bevy_ecs/src/error/command_handling.rs index d85ad4a87e..13ec866ec1 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, @@ -7,22 +9,19 @@ use crate::{ world::{error::EntityMutableFetchError, World}, }; -use super::{default_error_handler, BevyError, ErrorContext}; +use super::{BevyError, ErrorContext, ErrorHandler}; -/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into +/// Takes a [`Command`] that potentially returns a Result and uses a given error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. -pub trait HandleError { +pub trait HandleError: Send + 'static { /// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. - fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command; + fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command; /// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. - fn handle_error(self) -> impl Command - where - Self: Sized, - { - self.handle_error_with(default_error_handler()) - } + fn handle_error(self) -> impl Command; + /// Takes a [`Command`] that returns a Result and ignores any error that occurs. + fn ignore_error(self) -> impl Command; } impl HandleError> for C @@ -30,17 +29,35 @@ where C: Command>, E: Into, { - fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command { + fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command { move |world: &mut World| match self.apply(world) { Ok(_) => {} Err(err) => (error_handler)( err.into(), ErrorContext::Command { - name: type_name::().into(), + name: DebugName::type_name::(), }, ), } } + + fn handle_error(self) -> impl Command { + move |world: &mut World| match self.apply(world) { + Ok(_) => {} + Err(err) => world.default_error_handler()( + err.into(), + ErrorContext::Command { + name: DebugName::type_name::(), + }, + ), + } + } + + fn ignore_error(self) -> impl Command { + move |world: &mut World| { + let _ = self.apply(world); + } + } } impl HandleError for C @@ -52,6 +69,20 @@ where self.apply(world); } } + + #[inline] + fn handle_error(self) -> impl Command { + move |world: &mut World| { + self.apply(world); + } + } + + #[inline] + fn ignore_error(self) -> impl Command { + move |world: &mut World| { + self.apply(world); + } + } } impl HandleError for C @@ -63,10 +94,11 @@ where self } #[inline] - fn handle_error(self) -> impl Command - where - Self: Sized, - { + fn handle_error(self) -> impl Command { + self + } + #[inline] + fn ignore_error(self) -> impl Command { self } } diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index 688b599473..85a5a13297 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -1,9 +1,8 @@ -#[cfg(feature = "configurable_error_handler")] -use bevy_platform::sync::OnceLock; use core::fmt::Display; -use crate::{component::Tick, error::BevyError}; -use alloc::borrow::Cow; +use crate::{component::Tick, error::BevyError, prelude::Resource}; +use bevy_utils::prelude::DebugName; +use derive_more::derive::{Deref, DerefMut}; /// Context for a [`BevyError`] to aid in debugging. #[derive(Debug, PartialEq, Eq, Clone)] @@ -11,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, }, @@ -40,14 +39,14 @@ impl Display for ErrorContext { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::System { name, .. } => { - write!(f, "System `{}` failed", name) + write!(f, "System `{name}` failed") } - Self::Command { name } => write!(f, "Command `{}` failed", name), + Self::Command { name } => write!(f, "Command `{name}` failed"), Self::Observer { name, .. } => { - write!(f, "Observer `{}` failed", name) + write!(f, "Observer `{name}` failed") } Self::RunCondition { name, .. } => { - write!(f, "Run condition `{}` failed", name) + write!(f, "Run condition `{name}` failed") } } } @@ -55,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(), } } @@ -77,53 +76,6 @@ impl ErrorContext { } } -/// A global error handler. This can be set at startup, as long as it is set before -/// any uses. This should generally be configured _before_ initializing the app. -/// -/// This should be set inside of your `main` function, before initializing the Bevy app. -/// The value of this error handler can be accessed using the [`default_error_handler`] function, -/// which calls [`OnceLock::get_or_init`] to get the value. -/// -/// **Note:** this is only available when the `configurable_error_handler` feature of `bevy_ecs` (or `bevy`) is enabled! -/// -/// # Example -/// -/// ``` -/// # use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, warn}; -/// GLOBAL_ERROR_HANDLER.set(warn).expect("The error handler can only be set once, globally."); -/// // initialize Bevy App here -/// ``` -/// -/// To use this error handler in your app for custom error handling logic: -/// -/// ```rust -/// use bevy_ecs::error::{default_error_handler, GLOBAL_ERROR_HANDLER, BevyError, ErrorContext, panic}; -/// -/// fn handle_errors(error: BevyError, ctx: ErrorContext) { -/// let error_handler = default_error_handler(); -/// error_handler(error, ctx); -/// } -/// ``` -/// -/// # Warning -/// -/// As this can *never* be overwritten, library code should never set this value. -#[cfg(feature = "configurable_error_handler")] -pub static GLOBAL_ERROR_HANDLER: OnceLock = OnceLock::new(); - -/// The default error handler. This defaults to [`panic()`], -/// but if set, the [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization. -/// The `configurable_error_handler` feature must be enabled to change this from the panicking default behavior, -/// as there may be runtime overhead. -#[inline] -pub fn default_error_handler() -> fn(BevyError, ErrorContext) { - #[cfg(not(feature = "configurable_error_handler"))] - return panic; - - #[cfg(feature = "configurable_error_handler")] - return *GLOBAL_ERROR_HANDLER.get_or_init(|| panic); -} - macro_rules! inner { ($call:path, $e:ident, $c:ident) => { $call!( @@ -135,6 +87,25 @@ macro_rules! inner { }; } +/// Defines how Bevy reacts to errors. +pub type ErrorHandler = fn(BevyError, ErrorContext); + +/// Error handler to call when an error is not handled otherwise. +/// Defaults to [`panic()`]. +/// +/// When updated while a [`Schedule`] is running, it doesn't take effect for +/// that schedule until it's completed. +/// +/// [`Schedule`]: crate::schedule::Schedule +#[derive(Resource, Deref, DerefMut, Copy, Clone)] +pub struct DefaultErrorHandler(pub ErrorHandler); + +impl Default for DefaultErrorHandler { + fn default() -> Self { + Self(panic) + } +} + /// Error handler that panics with the system error. #[track_caller] #[inline] diff --git a/crates/bevy_ecs/src/error/mod.rs b/crates/bevy_ecs/src/error/mod.rs index 950deee3ec..231bdda940 100644 --- a/crates/bevy_ecs/src/error/mod.rs +++ b/crates/bevy_ecs/src/error/mod.rs @@ -7,8 +7,9 @@ //! All [`BevyError`]s returned by a system, observer or command are handled by an "error handler". By default, the //! [`panic`] error handler function is used, resulting in a panic with the error message attached. //! -//! You can change the default behavior by registering a custom error handler. -//! Modify the [`GLOBAL_ERROR_HANDLER`] value to set a custom error handler function for your entire app. +//! You can change the default behavior by registering a custom error handler: +//! Use [`DefaultErrorHandler`] to set a custom error handler function for a world, +//! or `App::set_error_handler` for a whole app. //! In practice, this is generally feature-flagged: panicking or loudly logging errors in development, //! and quietly logging or ignoring them in production to avoid crashing the app. //! @@ -33,10 +34,8 @@ //! The [`ErrorContext`] allows you to access additional details relevant to providing //! context surrounding the error – such as the system's [`name`] – in your error messages. //! -//! Remember to turn on the `configurable_error_handler` feature to set a global error handler! -//! //! ```rust, ignore -//! use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, BevyError, ErrorContext}; +//! use bevy_ecs::error::{BevyError, ErrorContext, DefaultErrorHandler}; //! use log::trace; //! //! fn my_error_handler(error: BevyError, ctx: ErrorContext) { @@ -48,10 +47,9 @@ //! } //! //! fn main() { -//! // This requires the "configurable_error_handler" feature to be enabled to be in scope. -//! GLOBAL_ERROR_HANDLER.set(my_error_handler).expect("The error handler can only be set once."); -//! -//! // Initialize your Bevy App here +//! let mut world = World::new(); +//! world.insert_resource(DefaultErrorHandler(my_error_handler)); +//! // Use your world here //! } //! ``` //! diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index d525ba2e57..89a3e10acd 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,35 +94,23 @@ 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. + /// Generates the [`EventKey`] for this event type. /// /// If this type has already been registered, - /// this will return the existing [`ComponentId`]. + /// this will return the existing [`EventKey`]. /// /// This is used by various dynamically typed observer APIs, /// such as [`World::trigger_targets_dynamic`]. /// /// # Warning /// - /// This method should not be overridden by implementors, - /// and should always correspond to the implementation of [`component_id`](Event::component_id). - fn register_component_id(world: &mut World) -> ComponentId { - world.register_component::>() + /// This method should not be overridden by implementers, + /// and should always correspond to the implementation of [`event_key`](Event::event_key). + fn register_event_key(world: &mut World) -> EventKey { + EventKey(world.register_component::>()) } - /// Fetches the [`ComponentId`] for this event type, + /// Fetches the [`EventKey`] for this event type, /// if it has already been generated. /// /// This is used by various dynamically typed observer APIs, @@ -82,13 +118,219 @@ pub trait Event: Send + Sync + 'static { /// /// # Warning /// - /// This method should not be overridden by implementors, - /// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id). - fn component_id(world: &World) -> Option { - world.component_id::>() + /// This method should not be overridden by implementers, + /// and should always correspond to the implementation of + /// [`register_event_key`](Event::register_event_key). + fn event_key(world: &World) -> Option { + world + .component_id::>() + .map(EventKey) } } +/// 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 +357,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 +367,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 +392,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 +420,23 @@ 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, } + +/// A unique identifier for an [`Event`], used by [observers]. +/// +/// You can look up the key for your event by calling the [`Event::event_key`] method. +/// +/// [observers]: crate::observer +#[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct EventKey(pub(crate) ComponentId); + +impl EventKey { + /// Returns the internal [`ComponentId`]. + #[inline] + pub(crate) fn component_id(&self) -> ComponentId { + self.0 + } +} diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index 66447b7de4..5175efb03a 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 /// } @@ -53,8 +54,8 @@ use { /// // run this once per update/frame /// events.update(); /// -/// // somewhere else: send an event -/// events.send(MyEvent { value: 1 }); +/// // somewhere else: write an event +/// events.write(MyEvent { value: 1 }); /// /// // somewhere else: read the events /// for event in cursor.read(&events) { @@ -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,28 +112,28 @@ 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 } - /// "Sends" an `event` by writing it to the current event buffer. + /// Writes an `event` to the current event buffer. /// [`EventReader`](super::EventReader)s can then read the event. - /// This method returns the [ID](`EventId`) of the sent `event`. + /// This method returns the [ID](`EventId`) of the written `event`. #[track_caller] - pub fn send(&mut self, event: E) -> EventId { - self.send_with_caller(event, MaybeLocation::caller()) + pub fn write(&mut self, event: E) -> EventId { + self.write_with_caller(event, MaybeLocation::caller()) } - pub(crate) fn send_with_caller(&mut self, event: E, caller: MaybeLocation) -> EventId { + pub(crate) fn write_with_caller(&mut self, event: E, caller: MaybeLocation) -> EventId { let event_id = EventId { id: self.event_count, caller, _marker: PhantomData, }; #[cfg(feature = "detailed_trace")] - tracing::trace!("Events::send() -> id: {}", event_id); + tracing::trace!("Events::write() -> id: {}", event_id); let event_instance = EventInstance { event_id, event }; @@ -142,30 +143,59 @@ impl Events { event_id } - /// Sends a list of `events` all at once, which can later be read by [`EventReader`](super::EventReader)s. - /// This is more efficient than sending each event individually. - /// This method returns the [IDs](`EventId`) of the sent `events`. + /// Writes a list of `events` all at once, which can later be read by [`EventReader`](super::EventReader)s. + /// This is more efficient than writing each event individually. + /// This method returns the [IDs](`EventId`) of the written `events`. #[track_caller] - pub fn send_batch(&mut self, events: impl IntoIterator) -> SendBatchIds { + pub fn write_batch(&mut self, events: impl IntoIterator) -> WriteBatchIds { let last_count = self.event_count; self.extend(events); - SendBatchIds { + WriteBatchIds { last_count, event_count: self.event_count, _marker: PhantomData, } } + /// Writes the default value of the event. Useful when the event is an empty struct. + /// This method returns the [ID](`EventId`) of the written `event`. + #[track_caller] + pub fn write_default(&mut self) -> EventId + where + E: Default, + { + self.write(Default::default()) + } + + /// "Sends" an `event` by writing it to the current event buffer. + /// [`EventReader`](super::EventReader)s can then read the event. + /// This method returns the [ID](`EventId`) of the sent `event`. + #[deprecated(since = "0.17.0", note = "Use `Events::write` instead.")] + #[track_caller] + pub fn send(&mut self, event: E) -> EventId { + self.write(event) + } + + /// Sends a list of `events` all at once, which can later be read by [`EventReader`](super::EventReader)s. + /// This is more efficient than sending each event individually. + /// This method returns the [IDs](`EventId`) of the sent `events`. + #[deprecated(since = "0.17.0", note = "Use `Events::write_batch` instead.")] + #[track_caller] + pub fn send_batch(&mut self, events: impl IntoIterator) -> WriteBatchIds { + self.write_batch(events) + } + /// Sends the default value of the event. Useful when the event is an empty struct. /// This method returns the [ID](`EventId`) of the sent `event`. + #[deprecated(since = "0.17.0", note = "Use `Events::write_default` instead.")] #[track_caller] pub fn send_default(&mut self) -> EventId where E: Default, { - self.send(Default::default()) + self.write_default() } /// Gets a new [`EventCursor`]. This will include all events already in the event buffers. @@ -286,7 +316,7 @@ impl Events { } } -impl Extend for Events { +impl Extend for Events { #[track_caller] fn extend(&mut self, iter: I) where @@ -321,13 +351,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 +366,7 @@ impl Default for EventSequence { } } -impl Deref for EventSequence { +impl Deref for EventSequence { type Target = Vec>; fn deref(&self) -> &Self::Target { @@ -344,20 +374,24 @@ impl Deref for EventSequence { } } -impl DerefMut for EventSequence { +impl DerefMut for EventSequence { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.events } } -/// [`Iterator`] over sent [`EventIds`](`EventId`) from a batch. -pub struct SendBatchIds { +/// [`Iterator`] over written [`EventIds`](`EventId`) from a batch. +pub struct WriteBatchIds { last_count: usize, event_count: usize, _marker: PhantomData, } -impl Iterator for SendBatchIds { +/// [`Iterator`] over sent [`EventIds`](`EventId`) from a batch. +#[deprecated(since = "0.17.0", note = "Use `WriteBatchIds` instead.")] +pub type SendBatchIds = WriteBatchIds; + +impl Iterator for WriteBatchIds { type Item = EventId; fn next(&mut self) -> Option { @@ -377,7 +411,7 @@ impl Iterator for SendBatchIds { } } -impl ExactSizeIterator for SendBatchIds { +impl ExactSizeIterator for WriteBatchIds { fn len(&self) -> usize { self.event_count.saturating_sub(self.last_count) } @@ -385,12 +419,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(); @@ -400,22 +433,22 @@ mod tests { assert_eq!(test_events.iter_current_update_events().count(), 0); test_events.update(); - // Sending one event - test_events.send(TestEvent); + // Writing one event + test_events.write(TestEvent); assert_eq!(test_events.len(), 1); assert_eq!(test_events.iter_current_update_events().count(), 1); test_events.update(); - // Sending two events on the next frame - test_events.send(TestEvent); - test_events.send(TestEvent); + // Writing two events on the next frame + test_events.write(TestEvent); + test_events.write(TestEvent); assert_eq!(test_events.len(), 3); // Events are double-buffered, so we see 1 + 2 = 3 assert_eq!(test_events.iter_current_update_events().count(), 2); test_events.update(); - // Sending zero events + // Writing zero events assert_eq!(test_events.len(), 2); // Events are double-buffered, so we see 2 + 0 = 2 assert_eq!(test_events.iter_current_update_events().count(), 0); } diff --git a/crates/bevy_ecs/src/event/event_cursor.rs b/crates/bevy_ecs/src/event/event_cursor.rs index ff15ef4931..f0460a9424 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`]. @@ -40,7 +41,7 @@ use core::marker::PhantomData; /// } /// /// for event in events_to_resend { -/// events.send(MyEvent); +/// events.write(MyEvent); /// } /// } /// @@ -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..77741a47ab 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,9 +11,10 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{Event, EventId}; -pub use bevy_ecs_macros::Event; -pub use collections::{Events, SendBatchIds}; +pub use base::{BufferedEvent, EntityEvent, Event, EventId, EventKey}; +pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event}; +#[expect(deprecated, reason = "`SendBatchIds` was renamed to `WriteBatchIds`.")] +pub use collections::{Events, SendBatchIds, WriteBatchIds}; pub use event_cursor::EventCursor; #[cfg(feature = "multi_threaded")] pub use iterators::EventParIter; @@ -38,17 +39,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::>() } @@ -65,7 +69,7 @@ mod tests { let mut reader_a: EventCursor = events.get_cursor(); - events.send(event_0); + events.write(event_0); assert_eq!( get_events(&events, &mut reader_a), @@ -91,7 +95,7 @@ mod tests { "second iteration of reader_b created after event results in zero events" ); - events.send(event_1); + events.write(event_1); let mut reader_c = events.get_cursor(); @@ -116,7 +120,7 @@ mod tests { let mut reader_d = events.get_cursor(); - events.send(event_2); + events.write(event_2); assert_eq!( get_events(&events, &mut reader_a), @@ -150,17 +154,17 @@ mod tests { assert!(reader.read(&events).next().is_none()); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); assert_eq!(*reader.read(&events).next().unwrap(), TestEvent { i: 0 }); assert_eq!(reader.read(&events).next(), None); - events.send(TestEvent { i: 1 }); + events.write(TestEvent { i: 1 }); clear_func(&mut events); assert!(reader.read(&events).next().is_none()); - events.send(TestEvent { i: 2 }); + events.write(TestEvent { i: 2 }); events.update(); - events.send(TestEvent { i: 3 }); + events.write(TestEvent { i: 3 }); assert!(reader .read(&events) @@ -182,22 +186,22 @@ mod tests { } #[test] - fn test_events_send_default() { + fn test_events_write_default() { let mut events = Events::::default(); - events.send_default(); + events.write_default(); let mut reader = events.get_cursor(); assert_eq!(get_events(&events, &mut reader), vec![EmptyTestEvent]); } #[test] - fn test_send_events_ids() { + fn test_write_events_ids() { let mut events = Events::::default(); let event_0 = TestEvent { i: 0 }; let event_1 = TestEvent { i: 1 }; let event_2 = TestEvent { i: 2 }; - let event_0_id = events.send(event_0); + let event_0_id = events.write(event_0); assert_eq!( events.get_event(event_0_id.id), @@ -205,7 +209,7 @@ mod tests { "Getting a sent event by ID should return the original event" ); - let mut event_ids = events.send_batch([event_1, event_2]); + let mut event_ids = events.write_batch([event_1, event_2]); let event_id = event_ids.next().expect("Event 1 must have been sent"); @@ -250,14 +254,14 @@ mod tests { let mut events = Events::::default(); let mut reader = events.get_cursor(); - events.send(TestEvent { i: 0 }); - events.send(TestEvent { i: 1 }); + events.write(TestEvent { i: 0 }); + events.write(TestEvent { i: 1 }); assert_eq!(reader.read(&events).count(), 2); let mut old_events = Vec::from_iter(events.update_drain()); assert!(old_events.is_empty()); - events.send(TestEvent { i: 2 }); + events.write(TestEvent { i: 2 }); assert_eq!(reader.read(&events).count(), 1); old_events.extend(events.update_drain()); @@ -275,7 +279,7 @@ mod tests { let mut events = Events::::default(); assert!(events.is_empty()); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); assert!(!events.is_empty()); events.update(); @@ -305,12 +309,12 @@ mod tests { let mut cursor = events.get_cursor(); assert!(cursor.read(&events).next().is_none()); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); let sent_event = cursor.read(&events).next().unwrap(); assert_eq!(sent_event, &TestEvent { i: 0 }); assert!(cursor.read(&events).next().is_none()); - events.send(TestEvent { i: 2 }); + events.write(TestEvent { i: 2 }); let sent_event = cursor.read(&events).next().unwrap(); assert_eq!(sent_event, &TestEvent { i: 2 }); assert!(cursor.read(&events).next().is_none()); @@ -327,7 +331,7 @@ mod tests { assert!(write_cursor.read_mut(&mut events).next().is_none()); assert!(read_cursor.read(&events).next().is_none()); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); let sent_event = write_cursor.read_mut(&mut events).next().unwrap(); assert_eq!(sent_event, &mut TestEvent { i: 0 }); *sent_event = TestEvent { i: 1 }; // Mutate whole event @@ -337,7 +341,7 @@ mod tests { ); assert!(read_cursor.read(&events).next().is_none()); - events.send(TestEvent { i: 2 }); + events.write(TestEvent { i: 2 }); let sent_event = write_cursor.read_mut(&mut events).next().unwrap(); assert_eq!(sent_event, &mut TestEvent { i: 2 }); sent_event.i = 3; // Mutate sub value @@ -357,7 +361,7 @@ mod tests { let mut events = Events::::default(); let mut reader = events.get_cursor(); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); assert_eq!(reader.len(&events), 1); reader.clear(&events); assert_eq!(reader.len(&events), 0); @@ -366,12 +370,12 @@ mod tests { #[test] fn test_event_cursor_len_update() { let mut events = Events::::default(); - events.send(TestEvent { i: 0 }); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); let reader = events.get_cursor(); assert_eq!(reader.len(&events), 2); events.update(); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); assert_eq!(reader.len(&events), 3); events.update(); assert_eq!(reader.len(&events), 1); @@ -382,10 +386,10 @@ mod tests { #[test] fn test_event_cursor_len_current() { let mut events = Events::::default(); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); let reader = events.get_cursor_current(); assert!(reader.is_empty(&events)); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); assert_eq!(reader.len(&events), 1); assert!(!reader.is_empty(&events)); } @@ -393,9 +397,9 @@ mod tests { #[test] fn test_event_cursor_iter_len_updated() { let mut events = Events::::default(); - events.send(TestEvent { i: 0 }); - events.send(TestEvent { i: 1 }); - events.send(TestEvent { i: 2 }); + events.write(TestEvent { i: 0 }); + events.write(TestEvent { i: 1 }); + events.write(TestEvent { i: 2 }); let mut reader = events.get_cursor(); let mut iter = reader.read(&events); assert_eq!(iter.len(), 3); @@ -417,7 +421,7 @@ mod tests { #[test] fn test_event_cursor_len_filled() { let mut events = Events::::default(); - events.send(TestEvent { i: 0 }); + events.write(TestEvent { i: 0 }); assert_eq!(events.get_cursor().len(&events), 1); assert!(!events.get_cursor().is_empty(&events)); } @@ -434,7 +438,7 @@ mod tests { let mut world = World::new(); world.init_resource::>(); for _ in 0..100 { - world.send_event(TestEvent { i: 1 }); + world.write_event(TestEvent { i: 1 }); } let mut schedule = Schedule::default(); @@ -476,7 +480,7 @@ mod tests { let mut world = World::new(); world.init_resource::>(); for _ in 0..100 { - world.send_event(TestEvent { i: 1 }); + world.write_event(TestEvent { i: 1 }); } let mut schedule = Schedule::default(); schedule.add_systems( @@ -525,20 +529,20 @@ mod tests { }); reader.initialize(&mut world); - let last = reader.run((), &mut world); + let last = reader.run((), &mut world).unwrap(); assert!(last.is_none(), "EventReader should be empty"); - world.send_event(TestEvent { i: 0 }); - let last = reader.run((), &mut world); + world.write_event(TestEvent { i: 0 }); + let last = reader.run((), &mut world).unwrap(); assert_eq!(last, Some(TestEvent { i: 0 })); - world.send_event(TestEvent { i: 1 }); - world.send_event(TestEvent { i: 2 }); - world.send_event(TestEvent { i: 3 }); - let last = reader.run((), &mut world); + world.write_event(TestEvent { i: 1 }); + world.write_event(TestEvent { i: 2 }); + world.write_event(TestEvent { i: 3 }); + let last = reader.run((), &mut world).unwrap(); assert_eq!(last, Some(TestEvent { i: 3 })); - let last = reader.run((), &mut world); + let last = reader.run((), &mut world).unwrap(); assert!(last.is_none(), "EventReader should be empty"); } @@ -555,20 +559,20 @@ mod tests { }); mutator.initialize(&mut world); - let last = mutator.run((), &mut world); + let last = mutator.run((), &mut world).unwrap(); assert!(last.is_none(), "EventMutator should be empty"); - world.send_event(TestEvent { i: 0 }); - let last = mutator.run((), &mut world); + world.write_event(TestEvent { i: 0 }); + let last = mutator.run((), &mut world).unwrap(); assert_eq!(last, Some(TestEvent { i: 0 })); - world.send_event(TestEvent { i: 1 }); - world.send_event(TestEvent { i: 2 }); - world.send_event(TestEvent { i: 3 }); - let last = mutator.run((), &mut world); + world.write_event(TestEvent { i: 1 }); + world.write_event(TestEvent { i: 2 }); + world.write_event(TestEvent { i: 3 }); + let last = mutator.run((), &mut world).unwrap(); assert_eq!(last, Some(TestEvent { i: 3 })); - let last = mutator.run((), &mut world); + let last = mutator.run((), &mut world).unwrap(); assert!(last.is_none(), "EventMutator should be empty"); } @@ -579,11 +583,11 @@ mod tests { let mut world = World::new(); world.init_resource::>(); - world.send_event(TestEvent { i: 0 }); - world.send_event(TestEvent { i: 1 }); - world.send_event(TestEvent { i: 2 }); - world.send_event(TestEvent { i: 3 }); - world.send_event(TestEvent { i: 4 }); + world.write_event(TestEvent { i: 0 }); + world.write_event(TestEvent { i: 1 }); + world.write_event(TestEvent { i: 2 }); + world.write_event(TestEvent { i: 3 }); + world.write_event(TestEvent { i: 4 }); let mut schedule = Schedule::default(); schedule.add_systems(|mut events: EventReader| { @@ -605,11 +609,11 @@ mod tests { let mut world = World::new(); world.init_resource::>(); - world.send_event(TestEvent { i: 0 }); - world.send_event(TestEvent { i: 1 }); - world.send_event(TestEvent { i: 2 }); - world.send_event(TestEvent { i: 3 }); - world.send_event(TestEvent { i: 4 }); + world.write_event(TestEvent { i: 0 }); + world.write_event(TestEvent { i: 1 }); + world.write_event(TestEvent { i: 2 }); + world.write_event(TestEvent { i: 3 }); + world.write_event(TestEvent { i: 4 }); let mut schedule = Schedule::default(); schedule.add_systems(|mut events: EventReader| { 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..847cbf3e16 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, /// } @@ -89,7 +89,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// }); /// }); /// for value in 0..100 { - /// world.send_event(MyEvent { value }); + /// world.write_event(MyEvent { value }); /// } /// schedule.run(&mut world); /// let Counter(counter) = world.remove_resource::().unwrap(); @@ -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..2e135eab2f 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, /// } @@ -61,7 +61,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// }); /// }); /// for value in 0..100 { - /// world.send_event(MyEvent { value }); + /// world.write_event(MyEvent { value }); /// } /// schedule.run(&mut world); /// let Counter(counter) = world.remove_resource::().unwrap(); @@ -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 231f792f68..3a69a11e4e 100644 --- a/crates/bevy_ecs/src/event/registry.rs +++ b/crates/bevy_ecs/src/event/registry.rs @@ -1,15 +1,15 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::{DetectChangesMut, MutUntyped}, - component::{ComponentId, Tick}, - event::{Event, Events}, + component::Tick, + event::{BufferedEvent, EventKey, Events}, resource::Resource, world::World, }; #[doc(hidden)] struct RegisteredEvent { - component_id: ComponentId, + event_key: EventKey, // Required to flush the secondary buffer and drop events even if left unchanged. previously_updated: bool, // SAFETY: The component ID and the function must be used to fetch the Events resource @@ -45,13 +45,13 @@ 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::>(); let mut registry = world.get_resource_or_init::(); registry.event_updates.push(RegisteredEvent { - component_id, + event_key: EventKey(component_id), previously_updated: false, update: |ptr| { // SAFETY: The resource was initialized with the type Events. @@ -66,7 +66,9 @@ impl EventRegistry { pub fn run_updates(&mut self, world: &mut World, last_change_tick: Tick) { for registered_event in &mut self.event_updates { // Bypass the type ID -> Component ID lookup with the cached component ID. - if let Some(events) = world.get_resource_mut_by_id(registered_event.component_id) { + if let Some(events) = + world.get_resource_mut_by_id(registered_event.event_key.component_id()) + { let has_changed = events.has_changed_since(last_change_tick); if registered_event.previously_updated || has_changed { // SAFETY: The update function pointer is called with the resource @@ -81,13 +83,13 @@ impl EventRegistry { } } - /// Removes an event from the world and it's associated [`EventRegistry`]. - pub fn deregister_events(world: &mut World) { + /// Removes an event from the world and its associated [`EventRegistry`]. + pub fn deregister_events(world: &mut World) { let component_id = world.init_resource::>(); let mut registry = world.get_resource_or_init::(); registry .event_updates - .retain(|e| e.component_id != component_id); + .retain(|e| e.event_key.component_id() != component_id); world.remove_resource::>(); } } diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index 5854ab34fb..015e59891c 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, WriteBatchIds}, system::{ResMut, SystemParam}, }; -/// Sends events of type `T`. +/// Writes [`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 @@ -34,14 +34,14 @@ use bevy_ecs::{ /// /// `EventWriter` can only write events of one specific type, which must be known at compile-time. /// This is not a problem most of the time, but you may find a situation where you cannot know -/// ahead of time every kind of event you'll need to send. In this case, you can use the "type-erased event" pattern. +/// ahead of time every kind of event you'll need to write. In this case, you can use the "type-erased event" pattern. /// /// ``` /// # 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 +/// fn write_untyped(mut commands: Commands) { +/// // Write an event of a specific type without having to declare that /// // type as a SystemParam. /// // /// // Effectively, we're just moving the type parameter from the /type/ to the /method/, @@ -51,7 +51,7 @@ use bevy_ecs::{ /// // NOTE: the event won't actually be sent until commands get applied during /// // apply_deferred. /// commands.queue(|w: &mut World| { -/// w.send_event(MyEvent); +/// w.write_event(MyEvent); /// }); /// } /// ``` @@ -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`. /// @@ -72,18 +72,18 @@ impl<'w, E: Event> EventWriter<'w, E> { #[doc(alias = "send")] #[track_caller] pub fn write(&mut self, event: E) -> EventId { - self.events.send(event) + self.events.write(event) } - /// Sends a list of `events` all at once, which can later be read by [`EventReader`](super::EventReader)s. - /// This is more efficient than sending each event individually. + /// Writes a list of `events` all at once, which can later be read by [`EventReader`](super::EventReader)s. + /// This is more efficient than writing each event individually. /// This method returns the [IDs](`EventId`) of the written `events`. /// /// See [`Events`] for details. #[doc(alias = "send_batch")] #[track_caller] - pub fn write_batch(&mut self, events: impl IntoIterator) -> SendBatchIds { - self.events.send_batch(events) + pub fn write_batch(&mut self, events: impl IntoIterator) -> WriteBatchIds { + self.events.write_batch(events) } /// Writes the default value of the event. Useful when the event is an empty struct. @@ -96,6 +96,6 @@ impl<'w, E: Event> EventWriter<'w, E> { where E: Default, { - self.events.send_default() + self.events.write_default() } } diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 158219d547..d325d5756c 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -10,8 +10,9 @@ use crate::reflect::{ReflectComponent, ReflectFromWorld}; use crate::{ bundle::Bundle, - component::{Component, HookContext}, + component::Component, entity::Entity, + lifecycle::HookContext, relationship::{RelatedSpawner, RelatedSpawnerCommands}, system::EntityCommands, world::{DeferredWorld, EntityWorldMut, FromWorld, World}, @@ -19,9 +20,11 @@ use crate::{ use alloc::{format, string::String, vec::Vec}; #[cfg(feature = "bevy_reflect")] 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. @@ -96,9 +99,14 @@ use log::warn; feature = "bevy_reflect", reflect(Component, PartialEq, Debug, FromWorld, Clone) )] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] #[relationship(relationship_target = Children)] #[doc(alias = "IsChild", alias = "Parent")] -pub struct ChildOf(pub Entity); +pub struct ChildOf(#[entities] pub Entity); impl ChildOf { /// The parent entity of this child entity. @@ -286,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 { @@ -297,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) @@ -311,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], @@ -366,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]) @@ -376,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) @@ -390,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], @@ -425,19 +455,18 @@ pub fn validate_parent_has_component( let Some(child_of) = entity_ref.get::() else { return; }; - if !world - .get_entity(child_of.parent()) - .is_ok_and(|e| e.contains::()) - { + let parent = child_of.parent(); + if !world.get_entity(parent).is_ok_and(|e| e.contains::()) { // 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://bevyengine.org/learn/errors/b0004", + "warning[B0004]: {}{name} with the {ty_name} component has a parent ({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), + || format!("Entity {entity}"), |s| format!("The {s} entity") ), ); @@ -633,6 +662,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() { @@ -690,6 +742,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/label.rs b/crates/bevy_ecs/src/label.rs index c404c563bd..9d3f6f838d 100644 --- a/crates/bevy_ecs/src/label.rs +++ b/crates/bevy_ecs/src/label.rs @@ -12,9 +12,6 @@ pub use alloc::boxed::Box; /// An object safe version of [`Eq`]. This trait is automatically implemented /// for any `'static` type that implements `Eq`. pub trait DynEq: Any { - /// Casts the type to `dyn Any`. - fn as_any(&self) -> &dyn Any; - /// This method tests for `self` and `other` values to be equal. /// /// Implementers should avoid returning `true` when the underlying types are @@ -29,12 +26,8 @@ impl DynEq for T where T: Any + Eq, { - fn as_any(&self) -> &dyn Any { - self - } - fn dyn_eq(&self, other: &dyn DynEq) -> bool { - if let Some(other) = other.as_any().downcast_ref::() { + if let Some(other) = (other as &dyn Any).downcast_ref::() { return self == other; } false @@ -44,9 +37,6 @@ where /// An object safe version of [`Hash`]. This trait is automatically implemented /// for any `'static` type that implements `Hash`. pub trait DynHash: DynEq { - /// Casts the type to `dyn Any`. - fn as_dyn_eq(&self) -> &dyn DynEq; - /// Feeds this value into the given [`Hasher`]. fn dyn_hash(&self, state: &mut dyn Hasher); } @@ -58,10 +48,6 @@ impl DynHash for T where T: DynEq + Hash, { - fn as_dyn_eq(&self) -> &dyn DynEq { - self - } - fn dyn_hash(&self, mut state: &mut dyn Hasher) { T::hash(self, &mut state); self.type_id().hash(&mut state); @@ -120,7 +106,7 @@ macro_rules! define_label { ) => { $(#[$label_attr])* - pub trait $label_trait_name: 'static + Send + Sync + ::core::fmt::Debug { + pub trait $label_trait_name: Send + Sync + ::core::fmt::Debug + $crate::label::DynEq + $crate::label::DynHash { $($trait_extra_methods)* @@ -129,12 +115,6 @@ macro_rules! define_label { ///`. fn dyn_clone(&self) -> $crate::label::Box; - /// Casts this value to a form where it can be compared with other type-erased values. - fn as_dyn_eq(&self) -> &dyn $crate::label::DynEq; - - /// Feeds this value into the given [`Hasher`]. - fn dyn_hash(&self, state: &mut dyn ::core::hash::Hasher); - /// Returns an [`Interned`] value corresponding to `self`. fn intern(&self) -> $crate::intern::Interned where Self: Sized { @@ -151,15 +131,6 @@ macro_rules! define_label { (**self).dyn_clone() } - /// Casts this value to a form where it can be compared with other type-erased values. - fn as_dyn_eq(&self) -> &dyn $crate::label::DynEq { - (**self).as_dyn_eq() - } - - fn dyn_hash(&self, state: &mut dyn ::core::hash::Hasher) { - (**self).dyn_hash(state); - } - fn intern(&self) -> Self { *self } @@ -167,7 +138,7 @@ macro_rules! define_label { impl PartialEq for dyn $label_trait_name { fn eq(&self, other: &Self) -> bool { - self.as_dyn_eq().dyn_eq(other.as_dyn_eq()) + self.dyn_eq(other) } } @@ -188,7 +159,7 @@ macro_rules! define_label { use ::core::ptr; // Test that both the type id and pointer address are equivalent. - self.as_dyn_eq().type_id() == other.as_dyn_eq().type_id() + self.type_id() == other.type_id() && ptr::addr_eq(ptr::from_ref::(self), ptr::from_ref::(other)) } @@ -196,7 +167,7 @@ macro_rules! define_label { use ::core::{hash::Hash, ptr}; // Hash the type id... - self.as_dyn_eq().type_id().hash(state); + self.type_id().hash(state); // ...and the pointer address. // Cast to a unit `()` first to discard any pointer metadata. diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 0a2e1862dd..8a07cdc8e1 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -13,8 +13,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![expect(unsafe_code, reason = "Unsafe code is used to improve performance.")] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -41,6 +41,7 @@ pub mod event; pub mod hierarchy; pub mod intern; pub mod label; +pub mod lifecycle; pub mod name; pub mod never; pub mod observer; @@ -48,7 +49,6 @@ pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod relationship; -pub mod removal_detection; pub mod resource; pub mod schedule; pub mod spawn; @@ -59,11 +59,18 @@ pub mod world; pub use bevy_ptr as ptr; +#[cfg(feature = "hotpatching")] +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}, @@ -71,18 +78,24 @@ pub mod prelude { component::Component, entity::{ContainsEntity, Entity, EntityMapper}, error::{BevyError, Result}, - event::{Event, EventMutator, EventReader, EventWriter, Events}, + event::{ + BufferedEvent, EntityEvent, Event, EventKey, EventMutator, EventReader, EventWriter, + Events, + }, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, + 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, - removal_detection::RemovedComponents, resource::Resource, schedule::{ - common_conditions::*, ApplyDeferred, Condition, IntoScheduleConfigs, IntoSystemSet, - Schedule, Schedules, SystemSet, + common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, + Schedules, SystemCondition, SystemSet, }, spawn::{Spawn, SpawnRelated}, system::{ @@ -93,7 +106,7 @@ pub mod prelude { }, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, - FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, + FromWorld, World, }, }; @@ -123,6 +136,13 @@ pub mod __macro_exports { pub use alloc::vec::Vec; } +/// Event sent when a hotpatch happens. +/// +/// Systems should refresh their inner pointers. +#[cfg(feature = "hotpatching")] +#[derive(Event, BufferedEvent, Default)] +pub struct HotPatched; + #[cfg(test)] mod tests { use crate::{ @@ -1228,7 +1248,6 @@ mod tests { .components() .get_resource_id(TypeId::of::()) .unwrap(); - let archetype_component_id = world.storages().resources.get(resource_id).unwrap().id(); assert_eq!(world.resource::().0, 123); assert!(world.contains_resource::()); @@ -1290,14 +1309,6 @@ mod tests { resource_id, current_resource_id, "resource id does not change after removing / re-adding" ); - - let current_archetype_component_id = - world.storages().resources.get(resource_id).unwrap().id(); - - assert_eq!( - archetype_component_id, current_archetype_component_id, - "resource archetype component id does not change after removing / re-adding" - ); } #[test] @@ -1562,9 +1573,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()); @@ -1629,7 +1638,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(); @@ -1644,7 +1653,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" ); @@ -2579,7 +2588,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)] @@ -2597,7 +2606,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 new file mode 100644 index 0000000000..7c3f1ffa4a --- /dev/null +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -0,0 +1,649 @@ +//! This module contains various tools to allow you to react to component insertion or removal, +//! as well as entity spawning and despawning. +//! +//! There are four main ways to react to these lifecycle events: +//! +//! 1. Using component hooks, which act as inherent constructors and destructors for components. +//! 2. Using [observers], which are a user-extensible way to respond to events, including component lifecycle events. +//! 3. Using the [`RemovedComponents`] system parameter, which offers an event-style interface. +//! 4. Using the [`Added`] query filter, which checks each component to see if it has been added since the last time a system ran. +//! +//! [observers]: crate::observer +//! [`Added`]: crate::query::Added +//! +//! # Types of lifecycle events +//! +//! 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: +//! +//! - [`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, [`Add`] hooks are evaluated before [`Insert`]. +//! +//! Next, we have lifecycle events that are triggered when a component is removed from an entity: +//! +//! - [`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. +//! +//! [`Replace`] hooks are evaluated before [`Remove`], then finally [`Despawn`] hooks are evaluated. +//! +//! [`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, [`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 [`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. +//! +//! ## Lifecycle events and component types +//! +//! Despite the absence of generics, each lifecycle event is associated with a specific component. +//! When defining a component hook for a [`Component`] type, that component is used. +//! When listening to lifecycle events for observers, the `B: Bundle` generic is used. +//! +//! Each of these lifecycle events also corresponds to a fixed [`ComponentId`], +//! which are assigned during [`World`] initialization. +//! 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::{ + BufferedEvent, EntityEvent, Event, EventCursor, EventId, EventIterator, + EventIteratorWithId, EventKey, Events, + }, + query::FilteredAccessSet, + relationship::RelationshipHookMode, + storage::SparseSet, + system::{Local, ReadOnlySystemParam, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, +}; + +use derive_more::derive::Into; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +use core::{ + fmt::Debug, + iter, + marker::PhantomData, + ops::{Deref, DerefMut}, + option, +}; + +/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. +pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); + +/// Context provided to a [`ComponentHook`]. +#[derive(Clone, Copy, Debug)] +pub struct HookContext { + /// The [`Entity`] this hook was invoked for. + pub entity: Entity, + /// The [`ComponentId`] this hook was invoked for. + pub component_id: ComponentId, + /// The caller location is `Some` if the `track_caller` feature is enabled. + pub caller: MaybeLocation, + /// Configures how relationship hooks will run + pub relationship_hook_mode: RelationshipHookMode, +} + +/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. +/// +/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. +/// These are intended to be used for structural side effects that need to happen when a component is added or removed, +/// and are not intended for general-purpose logic. +/// +/// For example, you might use a hook to update a cached index when a component is added, +/// to clean up resources when a component is removed, +/// or to keep hierarchical data structures across entities in sync. +/// +/// This information is stored in the [`ComponentInfo`](crate::component::ComponentInfo) of the associated component. +/// +/// There are two ways of configuring hooks for a component: +/// 1. Defining the relevant hooks on the [`Component`] implementation +/// 2. Using the [`World::register_component_hooks`] method +/// +/// # Example +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_platform::collections::HashSet; +/// +/// #[derive(Component)] +/// struct MyTrackedComponent; +/// +/// #[derive(Resource, Default)] +/// struct TrackedEntities(HashSet); +/// +/// let mut world = World::new(); +/// world.init_resource::(); +/// +/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks +/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); +/// assert!(tracked_component_query.iter(&world).next().is_none()); +/// +/// world.register_component_hooks::().on_add(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.insert(context.entity); +/// }); +/// +/// world.register_component_hooks::().on_remove(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.remove(&context.entity); +/// }); +/// +/// let entity = world.spawn(MyTrackedComponent).id(); +/// let tracked_entities = world.resource::(); +/// assert!(tracked_entities.0.contains(&entity)); +/// +/// world.despawn(entity); +/// let tracked_entities = world.resource::(); +/// assert!(!tracked_entities.0.contains(&entity)); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ComponentHooks { + pub(crate) on_add: Option, + pub(crate) on_insert: Option, + pub(crate) on_replace: Option, + pub(crate) on_remove: Option, + pub(crate) on_despawn: Option, +} + +impl ComponentHooks { + pub(crate) fn update_from_component(&mut self) -> &mut Self { + if let Some(hook) = C::on_add() { + self.on_add(hook); + } + if let Some(hook) = C::on_insert() { + self.on_insert(hook); + } + if let Some(hook) = C::on_replace() { + self.on_replace(hook); + } + if let Some(hook) = C::on_remove() { + self.on_remove(hook); + } + if let Some(hook) = C::on_despawn() { + self.on_despawn(hook); + } + + self + } + + /// Register a [`ComponentHook`] that will be run when this component is added to an entity. + /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as + /// adding all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_add` hook + pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_add(hook) + .expect("Component already has an on_add hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// or replaced. + /// + /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_insert` hook + pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_insert(hook) + .expect("Component already has an on_insert hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, + /// such as being replaced (with `.insert`) or removed. + /// + /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, + /// allowing access to the previous data just before it is dropped. + /// This hook does *not* run if the entity did not already have this component. + /// + /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_replace` hook + pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_replace(hook) + .expect("Component already has an on_replace hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// Despawning an entity counts as removing all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_remove` hook + pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_remove(hook) + .expect("Component already has an on_remove hook") + } + + /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_despawn` hook + pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_despawn(hook) + .expect("Component already has an on_despawn hook") + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. + /// + /// This is a fallible version of [`Self::on_add`]. + /// + /// Returns `None` if the component already has an `on_add` hook. + pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_add.is_some() { + return None; + } + self.on_add = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// + /// This is a fallible version of [`Self::on_insert`]. + /// + /// Returns `None` if the component already has an `on_insert` hook. + pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_insert.is_some() { + return None; + } + self.on_insert = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed + /// + /// This is a fallible version of [`Self::on_replace`]. + /// + /// Returns `None` if the component already has an `on_replace` hook. + pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_replace.is_some() { + return None; + } + self.on_replace = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// + /// This is a fallible version of [`Self::on_remove`]. + /// + /// Returns `None` if the component already has an `on_remove` hook. + pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_remove.is_some() { + return None; + } + self.on_remove = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// This is a fallible version of [`Self::on_despawn`]. + /// + /// Returns `None` if the component already has an `on_despawn` hook. + pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_despawn.is_some() { + return None; + } + self.on_despawn = Some(hook); + Some(self) + } +} + +/// [`EventKey`] for [`Add`] +pub const ADD: EventKey = EventKey(ComponentId::new(0)); +/// [`EventKey`] for [`Insert`] +pub const INSERT: EventKey = EventKey(ComponentId::new(1)); +/// [`EventKey`] for [`Replace`] +pub const REPLACE: EventKey = EventKey(ComponentId::new(2)); +/// [`EventKey`] for [`Remove`] +pub const REMOVE: EventKey = EventKey(ComponentId::new(3)); +/// [`EventKey`] for [`Despawn`] +pub const DESPAWN: EventKey = EventKey(ComponentId::new(4)); + +/// Trigger emitted when a component is inserted onto an entity that does not already have that +/// component. Runs before `Insert`. +/// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[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 `Add`, if it ran. +/// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[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, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[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, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[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, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[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, BufferedEvent, Debug, Clone, Into)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] +pub struct RemovedComponentEntity(Entity); + +/// Wrapper around a [`EventCursor`] so that we +/// can differentiate events between components. +#[derive(Debug)] +pub struct RemovedComponentReader +where + T: Component, +{ + reader: EventCursor, + marker: PhantomData, +} + +impl Default for RemovedComponentReader { + fn default() -> Self { + Self { + reader: Default::default(), + marker: PhantomData, + } + } +} + +impl Deref for RemovedComponentReader { + type Target = EventCursor; + fn deref(&self) -> &Self::Target { + &self.reader + } +} + +impl DerefMut for RemovedComponentReader { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.reader + } +} + +/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. +#[derive(Default, Debug)] +pub struct RemovedComponentEvents { + event_sets: SparseSet>, +} + +impl RemovedComponentEvents { + /// Creates an empty storage buffer for component removal events. + pub fn new() -> Self { + Self::default() + } + + /// For each type of component, swaps the event buffers and clears the oldest event buffer. + /// In general, this should be called once per frame/update. + pub fn update(&mut self) { + for (_component_id, events) in self.event_sets.iter_mut() { + events.update(); + } + } + + /// Returns an iterator over components and their entity events. + pub fn iter(&self) -> impl Iterator)> { + self.event_sets.iter() + } + + /// Gets the event storage for a given component. + pub fn get( + &self, + component_id: impl Into, + ) -> Option<&Events> { + self.event_sets.get(component_id.into()) + } + + /// Sends a removal event for the specified component. + #[deprecated(since = "0.17.0", note = "Use `RemovedComponentEvents:write` instead.")] + pub fn send(&mut self, component_id: impl Into, entity: Entity) { + self.write(component_id, entity); + } + + /// Writes a removal event for the specified component. + pub fn write(&mut self, component_id: impl Into, entity: Entity) { + self.event_sets + .get_or_insert_with(component_id.into(), Default::default) + .write(RemovedComponentEntity(entity)); + } +} + +/// A [`SystemParam`] that yields entities that had their `T` [`Component`] +/// removed or have been despawned with it. +/// +/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). +/// +/// Unlike hooks or observers (see the [lifecycle](crate) module docs), +/// this does not allow you to see which data existed before removal. +/// +/// If you are using `bevy_ecs` as a standalone crate, +/// note that the [`RemovedComponents`] list will not be automatically cleared for you, +/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). +/// +/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is +/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. +/// For the main world, this is delayed until after all `SubApp`s have run. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::lifecycle::RemovedComponents; +/// # +/// # #[derive(Component)] +/// # struct MyComponent; +/// fn react_on_removal(mut removed: RemovedComponents) { +/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); +/// } +/// # bevy_ecs::system::assert_is_system(react_on_removal); +/// ``` +#[derive(SystemParam)] +pub struct RemovedComponents<'w, 's, T: Component> { + component_id: ComponentIdFor<'s, T>, + reader: Local<'s, RemovedComponentReader>, + event_sets: &'w RemovedComponentEvents, +} + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIter<'a> = iter::Map< + iter::Flatten>>>, + fn(RemovedComponentEntity) -> Entity, +>; + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIterWithId<'a> = iter::Map< + iter::Flatten>>, + fn( + (&RemovedComponentEntity, EventId), + ) -> (Entity, EventId), +>; + +fn map_id_events( + (entity, id): (&RemovedComponentEntity, EventId), +) -> (Entity, EventId) { + (entity.clone().into(), id) +} + +// For all practical purposes, the api surface of `RemovedComponents` +// should be similar to `EventReader` to reduce confusion. +impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { + /// Fetch underlying [`EventCursor`]. + pub fn reader(&self) -> &EventCursor { + &self.reader + } + + /// Fetch underlying [`EventCursor`] mutably. + pub fn reader_mut(&mut self) -> &mut EventCursor { + &mut self.reader + } + + /// Fetch underlying [`Events`]. + pub fn events(&self) -> Option<&Events> { + self.event_sets.get(self.component_id.get()) + } + + /// Destructures to get a mutable reference to the `EventCursor` + /// and a reference to `Events`. + /// + /// This is necessary since Rust can't detect destructuring through methods and most + /// usecases of the reader uses the `Events` as well. + pub fn reader_mut_with_events( + &mut self, + ) -> Option<( + &mut RemovedComponentReader, + &Events, + )> { + self.event_sets + .get(self.component_id.get()) + .map(|events| (&mut *self.reader, events)) + } + + /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the + /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events + /// that happened before now. + pub fn read(&mut self) -> RemovedIter<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read(events).cloned()) + .into_iter() + .flatten() + .map(RemovedComponentEntity::into) + } + + /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. + pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read_with_id(events)) + .into_iter() + .flatten() + .map(map_id_events) + } + + /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. + pub fn len(&self) -> usize { + self.events() + .map(|events| self.reader.len(events)) + .unwrap_or(0) + } + + /// Returns `true` if there are no events available to read. + pub fn is_empty(&self) -> bool { + self.events() + .is_none_or(|events| self.reader.is_empty(events)) + } + + /// Consumes all available events. + /// + /// This means these events will not appear in calls to [`RemovedComponents::read()`] or + /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. + pub fn clear(&mut self) { + if let Some((reader, events)) = self.reader_mut_with_events() { + reader.clear(events); + } + } +} + +// SAFETY: Only reads World removed component events +unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} + +// SAFETY: no component value access. +unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { + type State = (); + type Item<'w, 's> = &'w RemovedComponentEvents; + + 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>( + _state: &'s mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + _change_tick: Tick, + ) -> Self::Item<'w, 's> { + world.removed_components() + } +} 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..544f2f1f6a --- /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_key: EventKey) -> &mut CachedObservers { + use crate::lifecycle::*; + + match event_key { + ADD => &mut self.add, + INSERT => &mut self.insert, + REPLACE => &mut self.replace, + REMOVE => &mut self.remove, + DESPAWN => &mut self.despawn, + _ => self.cache.entry(event_key).or_default(), + } + } + + /// Attempts to get the observers for the given `event_key`. + /// + /// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`], + /// use the [`EventKey`] constants from the [`lifecycle`](crate::lifecycle) module. + pub fn try_get_observers(&self, event_key: EventKey) -> Option<&CachedObservers> { + use crate::lifecycle::*; + + match event_key { + ADD => Some(&self.add), + INSERT => Some(&self.insert), + REPLACE => Some(&self.replace), + REMOVE => Some(&self.remove), + DESPAWN => Some(&self.despawn), + _ => self.cache.get(&event_key), + } + } + + /// This will run the observers of the given `event_key`, targeting the given `entity` and `components`. + pub(crate) fn invoke( + mut world: DeferredWorld, + event_key: EventKey, + 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_key) 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_key, + 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_key: EventKey) -> Option { + use crate::lifecycle::*; + + match event_key { + 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..f0f30cdf2c --- /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` [`EventKey`] _must_ match the actual value + /// of the event passed into the observer system. + pub unsafe fn with_event(mut self, event: EventKey) -> 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 [`EventKey`] 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) -> &[EventKey] { + &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_key = E::register_event_key(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_key); + 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 52% rename from crates/bevy_ecs/src/observer/entity_observer.rs rename to crates/bevy_ecs/src/observer/entity_cloning.rs index 2c2d42b1c9..bdbb1262bd 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_cloning.rs @@ -1,57 +1,17 @@ +//! Logic to track observers when cloning entities. + use crate::{ - component::{ - Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType, + component::ComponentCloneBehavior, + entity::{ + CloneByFilter, ComponentCloneCtx, EntityClonerBuilder, EntityMapper, SourceComponent, }, - entity::{ComponentCloneCtx, Entity, 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 { @@ -83,10 +43,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo .get_mut::(observer_entity) .expect("Source observer entity must have Observer"); observer_state.descriptor.entities.push(target); - let event_types = observer_state.descriptor.events.clone(); + let event_keys = 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); + for event_key in event_keys { + let observers = world.observers.get_observers_mut(event_key); if components.is_empty() { if let Some(map) = observers.entity_observers.get(&source).cloned() { observers.entity_observers.insert(target, map); @@ -97,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); } } } @@ -110,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] @@ -127,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 2343e66aa1..3fdc266f12 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1,541 +1,161 @@ -//! 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 [`Entity::PLACEHOLDER`]. - /// - /// 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) -> Entity { - 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: Entity, - /// 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 { - 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> { - 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: Entity, - 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 target != Entity::PLACEHOLDER { - if let Some(map) = observers.entity_observers.get(&target) { - 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 target != Entity::PLACEHOLDER { - if let Some(map) = component_observers.entity_map.get(&target) { - map.iter().for_each(&mut trigger_observer); - } - } - } - }); - } - - pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { - 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 [`On`]. + /// /// **Calling [`observe`](EntityWorldMut::observe) on the returned /// [`EntityWorldMut`] will observe the observer itself, which you very /// likely do not want.** @@ -548,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| { /// // ... /// }); /// ``` @@ -577,10 +197,10 @@ impl World { } pub(crate) fn trigger_with_caller(&mut self, mut event: E, caller: MaybeLocation) { - let event_id = E::register_component_id(self); - // SAFETY: We just registered `event_id` with the type of `event` + let event_key = E::register_event_key(self); + // SAFETY: We just registered `event_key` 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_key, &mut event, caller); } } @@ -590,47 +210,72 @@ impl World { /// or use the event after it has been modified by observers. #[track_caller] 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, ()) }; + let event_key = E::register_event_key(self); + // SAFETY: We just registered `event_key` with the type of `event` + unsafe { self.trigger_dynamic_ref_with_caller(event_key, 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_key: EventKey, + event_data: &mut E, + caller: MaybeLocation, + ) { + let mut world = DeferredWorld::from(self); + // SAFETY: `event_data` is accessible as the type represented by `event_key` + unsafe { + world.trigger_observers_with_data::<_, ()>( + event_key, + 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, caller: MaybeLocation, ) { - let event_id = E::register_component_id(self); - // SAFETY: We just registered `event_id` with the type of `event` + let event_key = E::register_event_key(self); + // SAFETY: We just registered `event_key` with the type of `event` unsafe { - self.trigger_targets_dynamic_ref_with_caller(event_id, &mut event, targets, caller); + self.trigger_targets_dynamic_ref_with_caller(event_key, &mut event, targets, caller); } } - /// 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) { - 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) }; + pub fn trigger_targets_ref( + &mut self, + event: &mut E, + targets: impl TriggerTargets, + ) { + let event_key = E::register_event_key(self); + // SAFETY: We just registered `event_key` with the type of `event` + unsafe { self.trigger_targets_dynamic_ref(event_key, 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. @@ -638,21 +283,21 @@ impl World { /// /// # Safety /// - /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. + /// Caller must ensure that `event_data` is accessible as the type represented by `event_key`. #[track_caller] - pub unsafe fn trigger_targets_dynamic( + pub unsafe fn trigger_targets_dynamic( &mut self, - event_id: ComponentId, + event_key: EventKey, mut event_data: E, targets: Targets, ) { - // SAFETY: `event_data` is accessible as the type represented by `event_id` + // SAFETY: `event_data` is accessible as the type represented by `event_key` unsafe { - self.trigger_targets_dynamic_ref(event_id, &mut event_data, targets); + self.trigger_targets_dynamic_ref(event_key, &mut event_data, targets); }; } - /// 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 @@ -660,16 +305,16 @@ impl World { /// /// # Safety /// - /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. + /// Caller must ensure that `event_data` is accessible as the type represented by `event_key`. #[track_caller] - pub unsafe fn trigger_targets_dynamic_ref( + pub unsafe fn trigger_targets_dynamic_ref( &mut self, - event_id: ComponentId, + event_key: EventKey, event_data: &mut E, targets: Targets, ) { self.trigger_targets_dynamic_ref_with_caller( - event_id, + event_key, event_data, targets, MaybeLocation::caller(), @@ -679,9 +324,9 @@ 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_key: EventKey, event_data: &mut E, targets: Targets, caller: MaybeLocation, @@ -689,11 +334,12 @@ impl World { let mut world = DeferredWorld::from(self); let mut entity_targets = targets.entities().peekable(); if entity_targets.peek().is_none() { - // SAFETY: `event_data` is accessible as the type represented by `event_id` + // SAFETY: `event_data` is accessible as the type represented by `event_key` unsafe { world.trigger_observers_with_data::<_, E::Traversal>( - event_id, - Entity::PLACEHOLDER, + event_key, + None, + None, targets.components(), event_data, false, @@ -702,11 +348,12 @@ impl World { }; } else { for target_entity in entity_targets { - // SAFETY: `event_data` is accessible as the type represented by `event_id` + // SAFETY: `event_data` is accessible as the type represented by `event_key` unsafe { world.trigger_observers_with_data::<_, E::Traversal>( - event_id, - target_entity, + event_key, + Some(target_entity), + Some(target_entity), targets.components(), event_data, E::AUTO_PROPAGATE, @@ -732,11 +379,13 @@ impl World { }; let descriptor = &observer_state.descriptor; - for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + for &event_key in &descriptor.events { + let cache = observers.get_observers_mut(event_key); 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 { @@ -751,18 +400,23 @@ impl World { .component_observers .entry(component) .or_insert_with(|| { - if let Some(flag) = Observers::is_archetype_cached(event_type) { + if let Some(flag) = Observers::is_archetype_cached(event_key) { archetypes.update_flags(component, flag, true); } CachedComponentObservers::default() }); 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); } } @@ -776,10 +430,10 @@ impl World { let archetypes = &mut self.archetypes; let observers = &mut self.observers; - for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + for &event_key in &descriptor.events { + let cache = observers.get_observers_mut(event_key); 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 @@ -797,22 +451,26 @@ 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(flag) = Observers::is_archetype_cached(event_key) { if let Some(by_component) = archetypes.by_component.get(component) { for archetype in by_component.keys() { let archetype = &mut archetypes.archetypes[archetype.index()]; @@ -845,7 +503,7 @@ mod tests { use crate::component::ComponentId; use crate::{ change_detection::MaybeLocation, - observer::{Observer, OnReplace}, + observer::{Observer, Replace}, prelude::*, traversal::Traversal, }; @@ -863,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, } @@ -885,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] @@ -899,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); @@ -921,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); @@ -945,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); @@ -971,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. @@ -995,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()).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()).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()).remove::(); }, ); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_b"); }); @@ -1031,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(); @@ -1047,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, @@ -1071,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_event_key(&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) @@ -1110,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"); }); @@ -1124,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(); @@ -1140,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."); }; @@ -1161,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"); }); @@ -1174,11 +824,11 @@ 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| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), Entity::PLACEHOLDER); res.observed("event_a"); }); @@ -1196,16 +846,16 @@ 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| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), entity); res.observed("a_2"); }); @@ -1231,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, ); @@ -1338,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), ); @@ -1358,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_event_key(&mut world); // SAFETY: we registered `event_a` above and it matches the type of EventA let observe = unsafe { @@ -1382,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); @@ -1411,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(); @@ -1441,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(); @@ -1471,7 +1127,7 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); @@ -1479,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); }, @@ -1501,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(); @@ -1538,7 +1194,7 @@ mod tests { let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("event"); }) .id(); @@ -1558,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(); @@ -1566,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); }, @@ -1575,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(); @@ -1603,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"); }); @@ -1625,7 +1281,7 @@ mod tests { world.init_resource::(); world.add_observer( - |trigger: Trigger, query: Query<&A>, mut res: ResMut| { + |trigger: On, query: Query<&A>, mut res: ResMut| { if query.get(trigger.target()).is_ok() { res.observed("event"); } @@ -1647,7 +1303,7 @@ 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()) .with_related_entities::(|rsc| { @@ -1668,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(); @@ -1676,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. @@ -1694,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); @@ -1707,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); }, ); @@ -1728,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); @@ -1742,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(); @@ -1763,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 ac1caa5ad0..f25e742eed 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,13 +1,13 @@ -use alloc::{boxed::Box, vec}; +//! Logic for evaluating observers, and storing functions inside of observers. + use core::any::Any; use crate::{ - component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType}, - error::{default_error_handler, ErrorContext}, - observer::{ObserverDescriptor, ObserverTrigger}, + error::ErrorContext, + observer::ObserverTrigger, prelude::*, query::DebugCheckedUnwrap, - system::{IntoObserverSystem, ObserverSystem}, + system::{ObserverSystem, RunSystemError}, world::DeferredWorld, }; use bevy_ptr::PtrMut; @@ -18,315 +18,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()); -/// 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)] -/// # 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()).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| { - 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, @@ -348,10 +40,8 @@ fn observer_system_runner>( return; } state.last_trigger_id = last_trigger; - // SAFETY: Observer was triggered so must have an `Observer` component. - let error_handler = unsafe { state.error_handler.debug_checked_unwrap() }; - let trigger: Trigger = Trigger::new( + let trigger: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { ptr.deref_mut() }, propagate, @@ -362,84 +52,50 @@ 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 }; // SAFETY: - // - `update_archetype_component_access` is called first // - there are no outstanding references to world except a private component // - system is an `ObserverSystem` so won't mutate world beyond the access of a `DeferredWorld` // and is never exclusive // - system is the same type erased system from above unsafe { - (*system).update_archetype_component_access(world); - match (*system).validate_param_unsafe(world) { - Ok(()) => { - if let Err(err) = (*system).run_unsafe(trigger, world) { - error_handler( - err, - ErrorContext::Observer { - name: (*system).name(), - last_run: (*system).get_last_run(), - }, - ); - }; - (*system).queue_deferred(world.into_deferred()); - } - Err(e) => { - if !e.skipped { - error_handler( - e.into(), - ErrorContext::Observer { - name: (*system).name(), - last_run: (*system).get_last_run(), - }, - ); - } - } - } + // Always refresh hotpatch pointers + // There's no guarantee that the `HotPatched` event would still be there once the observer is triggered. + #[cfg(feature = "hotpatching")] + (*system).refresh_hotpatch(); + + if let Err(RunSystemError::Failed(err)) = (*system) + .validate_param_unsafe(world) + .map_err(From::from) + .and_then(|()| (*system).run_unsafe(trigger, world)) + { + let handler = state + .error_handler + .unwrap_or_else(|| world.default_error_handler()); + handler( + err, + ErrorContext::Observer { + name: (*system).name(), + last_run: (*system).get_last_run(), + }, + ); + }; + (*system).queue_deferred(world.into_deferred()); } } -/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::component::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); - - if observe.error_handler.is_none() { - observe.error_handler = Some(default_error_handler()); - } - 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::{event::Event, observer::Trigger}; + use crate::{ + error::{ignore, DefaultErrorHandler}, + event::Event, + observer::On, + }; #[derive(Event)] struct TriggerEvent; @@ -447,7 +103,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()) } @@ -462,26 +118,33 @@ 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()) } + // Using observer error handler let mut world = World::default(); world.init_resource::(); - let observer = Observer::new(system).with_error_handler(crate::error::ignore); - world.spawn(observer); - Schedule::default().run(&mut world); + world.spawn(Observer::new(system).with_error_handler(ignore)); + world.trigger(TriggerEvent); + assert!(world.resource::().0); + + // Using world error handler + let mut world = World::default(); + world.init_resource::(); + world.spawn(Observer::new(system)); + // Test that the correct handler is used when the observer was added + // before the default handler + world.insert_resource(DefaultErrorHandler(ignore)); world.trigger(TriggerEvent); assert!(world.resource::().0); } #[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..5d6d665564 --- /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_key(&self) -> EventKey { + self.trigger.event_key + } + + /// 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_key: EventKey, + /// 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 3c1ff5262c..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; @@ -47,6 +48,8 @@ use variadics_please::all_tuples; /// - **[`Ref`].** /// Similar to change detection filters but it is used as a query fetch parameter. /// It exposes methods to check for changes to the wrapped component. +/// - **[`Mut`].** +/// Mutable component access, with change detection data. /// - **[`Has`].** /// Returns a bool indicating whether the entity has the specified component. /// @@ -161,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; /// } @@ -172,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) /// } @@ -288,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` @@ -320,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. @@ -335,12 +341,23 @@ 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` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Entity { type Fetch<'w> = (); @@ -348,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> { @@ -359,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) {} @@ -392,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 } } @@ -411,8 +435,14 @@ 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` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for EntityLocation { type Fetch<'w> = &'w Entities; @@ -422,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> { @@ -436,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) {} @@ -468,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() } } @@ -488,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 @@ -518,7 +561,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// match spawn_details.spawned_by().into_option() { /// Some(location) => println!(" by {:?}", location), /// None => println!() -/// } +/// } /// } /// } /// @@ -568,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> { @@ -584,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) {} @@ -618,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 @@ -648,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)] @@ -660,7 +716,7 @@ pub struct EntityFetch<'w> { /// SAFETY: /// `fetch` accesses all components in a readonly way. -/// This is sound because `update_component_access` and `update_archetype_component_access` set read access for all components and panic when appropriate. +/// This is sound because `update_component_access` sets read access for all components and panic when appropriate. /// Filters are unchanged. unsafe impl<'a> WorldQuery for EntityRef<'a> { type Fetch<'w> = EntityFetch<'w>; @@ -670,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> { @@ -686,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) { @@ -724,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 @@ -751,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>; @@ -760,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> { @@ -776,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) { @@ -814,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 @@ -838,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); @@ -849,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> { @@ -868,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, ) { @@ -878,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); } @@ -913,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 } @@ -939,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 @@ -970,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> { @@ -989,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, ) { @@ -999,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); } @@ -1034,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 } @@ -1058,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 @@ -1089,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> { @@ -1104,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, @@ -1128,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); } @@ -1159,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) @@ -1196,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> { @@ -1211,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, @@ -1235,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); } @@ -1267,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) @@ -1287,7 +1381,7 @@ where } /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for &Archetype { type Fetch<'w> = (&'w Entities, &'w Archetypes); @@ -1297,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> { @@ -1311,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) {} @@ -1343,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() }; @@ -1366,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< @@ -1382,11 +1489,12 @@ impl Clone for ReadFetch<'_, T> { *self } } + impl Copy for ReadFetch<'_, T> {} /// SAFETY: /// `fetch` accesses a single component in a readonly way. -/// This is sound because `update_component_access` and `update_archetype_component_access` add read access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds read access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for &T { @@ -1398,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, @@ -1409,7 +1517,7 @@ unsafe impl WorldQuery for &T { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components in this function, we just get a shared // reference to the sparse set, which is used to access the components in `Self::fetch`. unsafe { world.storages().sparse_sets.get(component_id) } @@ -1463,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); } @@ -1488,24 +1596,27 @@ 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 let table = unsafe { table.debug_checked_unwrap() }; // SAFETY: Caller ensures `table_row` is in range. - let item = unsafe { table.get(table_row.as_usize()) }; + let item = unsafe { table.get(table_row.index()) }; item.deref() }, |sparse_set| { @@ -1525,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< @@ -1549,11 +1666,12 @@ impl Clone for RefFetch<'_, T> { *self } } + impl Copy for RefFetch<'_, T> {} /// SAFETY: /// `fetch` accesses a single component in a readonly way. -/// This is sound because `update_component_access` and `update_archetype_component_access` add read access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds read access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { @@ -1565,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, @@ -1576,7 +1694,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components in this function, we just get a shared // reference to the sparse set, which is used to access the components in `Self::fetch`. unsafe { world.storages().sparse_sets.get(component_id) } @@ -1617,11 +1735,15 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { ) { let column = table.get_column(component_id).debug_checked_unwrap(); let table_data = Some(( - column.get_data_slice(table.entity_count()).into(), - column.get_added_ticks_slice(table.entity_count()).into(), - column.get_changed_ticks_slice(table.entity_count()).into(), + column.get_data_slice(table.entity_count() as usize).into(), column - .get_changed_by_slice(table.entity_count()) + .get_added_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_by_slice(table.entity_count() as usize) .map(Into::into), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table @@ -1635,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); } @@ -1660,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 @@ -1679,13 +1804,13 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let component = unsafe { table_components.get(table_row.as_usize()) }; + let component = unsafe { table_components.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let added = unsafe { added_ticks.get(table_row.as_usize()) }; + let added = unsafe { added_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; + let changed = unsafe { changed_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let caller = callers.map(|callers| unsafe { callers.get(table_row.as_usize()) }); + let caller = callers.map(|callers| unsafe { callers.get(table_row.index()) }); Ref { value: component.deref(), @@ -1720,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< @@ -1744,11 +1875,12 @@ impl Clone for WriteFetch<'_, T> { *self } } + impl Copy for WriteFetch<'_, T> {} /// SAFETY: /// `fetch` accesses a single component mutably. -/// This is sound because `update_component_access` and `update_archetype_component_access` add write access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds write access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { @@ -1760,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, @@ -1771,7 +1903,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components in this function, we just get a shared // reference to the sparse set, which is used to access the components in `Self::fetch`. unsafe { world.storages().sparse_sets.get(component_id) } @@ -1812,11 +1944,15 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { ) { let column = table.get_column(component_id).debug_checked_unwrap(); let table_data = Some(( - column.get_data_slice(table.entity_count()).into(), - column.get_added_ticks_slice(table.entity_count()).into(), - column.get_changed_ticks_slice(table.entity_count()).into(), + column.get_data_slice(table.entity_count() as usize).into(), column - .get_changed_by_slice(table.entity_count()) + .get_added_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_by_slice(table.entity_count() as usize) .map(Into::into), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table @@ -1830,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); } @@ -1855,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 @@ -1874,13 +2013,13 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let component = unsafe { table_components.get(table_row.as_usize()) }; + let component = unsafe { table_components.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let added = unsafe { added_ticks.get(table_row.as_usize()) }; + let added = unsafe { added_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; + let changed = unsafe { changed_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let caller = callers.map(|callers| unsafe { callers.get(table_row.as_usize()) }); + let caller = callers.map(|callers| unsafe { callers.get(table_row.index()) }); Mut { value: component.deref_mut(), @@ -1912,13 +2051,19 @@ 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. /// /// SAFETY: /// `fetch` accesses a single component mutably. -/// This is sound because `update_component_access` and `update_archetype_component_access` add write access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds write access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { @@ -1931,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, @@ -1970,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); } @@ -1998,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 } } @@ -2035,7 +2189,7 @@ impl Clone for OptionFetch<'_, T> { /// SAFETY: /// `fetch` might access any components that `T` accesses. -/// This is sound because `update_component_access` and `update_archetype_component_access` add the same accesses as `T`. +/// This is sound because `update_component_access` adds the same accesses as `T`. /// Filters are unchanged. unsafe impl WorldQuery for Option { type Fetch<'w> = OptionFetch<'w, T>; @@ -2049,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> { @@ -2065,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, ) { @@ -2081,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. @@ -2126,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 @@ -2215,12 +2382,12 @@ 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::()) } } /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Has { type Fetch<'w> = bool; @@ -2231,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> { @@ -2248,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, ) { @@ -2258,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); } @@ -2290,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 } } @@ -2309,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)>>`. @@ -2317,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." @@ -2339,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), @@ -2359,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, @@ -2398,7 +2592,7 @@ macro_rules! impl_anytuple_fetch { )] /// SAFETY: /// `fetch` accesses are a subset of the subqueries' accesses - /// This is sound because `update_component_access` and `update_archetype_component_access` adds accesses according to the implementations of all the subqueries. + /// This is sound because `update_component_access` adds accesses according to the implementations of all the subqueries. /// `update_component_access` replaces the filters with a disjunction where every element is a conjunction of the previous filters and the filters of one of the subqueries. /// This is sound because `matches_component_set` returns a disjunction of the results of the subqueries' implementations. unsafe impl<$($name: WorldQuery),*> WorldQuery for AnyOf<($($name,)*)> { @@ -2413,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),)*) @@ -2422,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 ) { @@ -2440,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; $( @@ -2512,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), @@ -2522,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) }), )*) } } @@ -2538,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)),)*) + } + } }; } @@ -2547,7 +2757,8 @@ all_tuples!( 0, 15, F, - S + i, + s ); all_tuples!( #[doc(fake_variadic)] @@ -2555,7 +2766,8 @@ all_tuples!( 0, 15, F, - S + S, + i ); /// [`WorldQuery`] used to nullify queries by turning `Query` into `Query>` @@ -2564,13 +2776,14 @@ all_tuples!( pub(crate) struct NopWorldQuery(PhantomData); /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. 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( @@ -2617,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` and `update_archetype_component_access` do nothing. +/// `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> { @@ -2656,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) {} @@ -2687,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 { @@ -2818,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 38c7cfcb32..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> { @@ -729,7 +743,7 @@ unsafe impl WorldQuery for Added { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components' ticks in this function, we just get a shared // reference to the sparse set, which is used to access the components' ticks in `Self::fetch`. unsafe { world.storages().sparse_sets.get(id) } @@ -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, @@ -817,7 +832,7 @@ unsafe impl QueryFilter for Added { // SAFETY: set_table was previously called let table = unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let tick = unsafe { table.get(table_row.as_usize()) }; + let tick = unsafe { table.get(table_row.index()) }; tick.deref().is_newer_than(fetch.last_run, fetch.this_run) }, @@ -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> { @@ -955,7 +970,7 @@ unsafe impl WorldQuery for Changed { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components' ticks in this function, we just get a shared // reference to the sparse set, which is used to access the components' ticks in `Self::fetch`. unsafe { world.storages().sparse_sets.get(id) } @@ -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, @@ -1044,7 +1060,7 @@ unsafe impl QueryFilter for Changed { // SAFETY: set_table was previously called let table = unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let tick = unsafe { table.get(table_row.as_usize()) }; + let tick = unsafe { table.get(table_row.index()) }; tick.deref().is_newer_than(fetch.last_run, fetch.this_run) }, @@ -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 fc89843493..eb49204434 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -19,6 +19,7 @@ use core::{ mem::MaybeUninit, ops::Range, }; +use nonmax::NonMaxU32; /// An [`Iterator`] over query results of a [`Query`](crate::system::Query). /// @@ -136,10 +137,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, storage: StorageId, - range: Option>, + 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. @@ -199,18 +200,14 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, table: &'w Table, - rows: Range, + 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; } - debug_assert!( - rows.end <= u32::MAX as usize, - "TableRow is only valid up to u32::MAX" - ); D::set_table(&mut self.cursor.fetch, &self.query_state.fetch_state, table); F::set_table( @@ -222,19 +219,32 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let entities = table.entities(); for row in rows { // SAFETY: Caller assures `row` in range of the current archetype. - let entity = unsafe { entities.get_unchecked(row) }; - let row = TableRow::from_usize(row); + let entity = unsafe { entities.get_unchecked(row as usize) }; + // SAFETY: This is from an exclusive range, so it can't be max. + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(row)) }; // 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); } @@ -254,10 +264,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, archetype: &'w Archetype, - indices: Range, + 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; @@ -279,12 +289,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let entities = archetype.entities(); for index in indices { // SAFETY: Caller assures `index` in range of the current archetype. - let archetype_entity = unsafe { entities.get_unchecked(index) }; + let archetype_entity = unsafe { entities.get_unchecked(index as usize) }; // SAFETY: set_archetype was called prior. // 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(), @@ -298,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(), @@ -315,7 +327,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// # Safety /// - all `indices` must be in `[0, archetype.len())`. /// - `archetype` must match D and F - /// - `archetype` must have the same length with it's table. + /// - `archetype` must have the same length as its table. /// - The query iteration must not be dense (i.e. `self.query_state.is_dense` must be false). #[inline] pub(super) unsafe fn fold_over_dense_archetype_range( @@ -323,22 +335,18 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, archetype: &'w Archetype, - rows: Range, + 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; } - debug_assert!( - rows.end <= u32::MAX as usize, - "TableRow is only valid up to u32::MAX" - ); let table = self.tables.get(archetype.table_id()).debug_checked_unwrap(); debug_assert!( archetype.len() == table.entity_count(), - "archetype and it's table must have the same length. " + "archetype and its table must have the same length. " ); D::set_archetype( @@ -356,19 +364,32 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let entities = table.entities(); for row in rows { // SAFETY: Caller assures `row` in range of the current archetype. - let entity = unsafe { *entities.get_unchecked(row) }; - let row = TableRow::from_usize(row); + let entity = unsafe { *entities.get_unchecked(row as usize) }; + // SAFETY: This is from an exclusive range, so it can't be max. + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(row)) }; // 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); } @@ -497,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()) } @@ -554,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()) } @@ -610,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, @@ -642,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, @@ -734,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, @@ -767,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, @@ -802,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, @@ -832,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, @@ -861,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. @@ -878,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 { @@ -895,7 +920,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> let max_size = self.cursor.max_remaining(self.tables, self.archetypes); let archetype_query = F::IS_ARCHETYPAL; let min_size = if archetype_query { max_size } else { 0 }; - (min_size, Some(max_size)) + (min_size as usize, Some(max_size as usize)) } #[inline] @@ -1015,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`] @@ -1044,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, + ) + } } } @@ -1053,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 { @@ -1175,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 { @@ -1205,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 @@ -1217,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, @@ -1341,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()) } @@ -1399,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()) } @@ -1456,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, @@ -1487,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, @@ -1581,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, @@ -1613,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, @@ -1647,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, @@ -1676,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, @@ -1726,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, @@ -1750,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 { @@ -1866,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 { @@ -1959,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`] @@ -1988,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: @@ -2012,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: @@ -2029,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 { @@ -2190,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 // @@ -2216,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()) @@ -2228,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. @@ -2245,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 { @@ -2275,7 +2324,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> Iterator .enumerate() .try_fold(0, |acc, (i, cursor)| { let n = cursor.max_remaining(self.tables, self.archetypes); - Some(acc + choose(n, K - i)?) + Some(acc + choose(n as usize, K - i)?) }); let archetype_query = F::IS_ARCHETYPAL; @@ -2317,9 +2366,9 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { fetch: D::Fetch<'w>, filter: F::Fetch<'w>, // length of the table or length of the archetype, depending on whether both `D`'s and `F`'s fetches are dense - current_len: usize, + current_len: u32, // either table row or archetype index, depending on whether both `D`'s and `F`'s fetches are dense - current_row: usize, + current_row: u32, } impl Clone for QueryIterationCursor<'_, '_, D, F> { @@ -2395,30 +2444,34 @@ 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 { // SAFETY: This must have been called previously in `next` as `current_row > 0` - let entity = unsafe { self.table_entities.get_unchecked(index) }; + let entity = unsafe { self.table_entities.get_unchecked(index as usize) }; // SAFETY: // - `set_table` must have been called previously either in `next` or before it. // - `*entity` and `index` are in the current table. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, *entity, - TableRow::from_usize(index), + // SAFETY: This is from an exclusive range, so it can't be max. + TableRow::new(NonMaxU32::new_unchecked(index)), )) } } else { // SAFETY: This must have been called previously in `next` as `current_row > 0` - let archetype_entity = unsafe { self.archetype_entities.get_unchecked(index) }; + let archetype_entity = + unsafe { self.archetype_entities.get_unchecked(index as usize) }; // SAFETY: // - `set_archetype` must have been called previously either in `next` or before it. // - `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(), @@ -2434,9 +2487,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// /// Note that if `F::IS_ARCHETYPAL`, the return value /// will be **the exact count of remaining values**. - fn max_remaining(&self, tables: &'w Tables, archetypes: &'w Archetypes) -> usize { + fn max_remaining(&self, tables: &'w Tables, archetypes: &'w Archetypes) -> u32 { let ids = self.storage_id_iter.clone(); - let remaining_matched: usize = if self.is_dense { + let remaining_matched: u32 = if self.is_dense { // SAFETY: The if check ensures that storage_id_iter stores TableIds unsafe { ids.map(|id| tables[id.table_id].entity_count()).sum() } } else { @@ -2460,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 @@ -2483,9 +2536,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // SAFETY: set_table was called prior. // `current_row` is a table row in range of the current table, because if it was not, then the above would have been executed. - let entity = unsafe { self.table_entities.get_unchecked(self.current_row) }; - let row = TableRow::from_usize(self.current_row); - if !F::filter_fetch(&mut self.filter, *entity, row) { + let entity = + 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(&query_state.filter_state, &mut self.filter, *entity, row) { self.current_row += 1; continue; } @@ -2495,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); @@ -2532,9 +2588,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // SAFETY: set_archetype was called prior. // `current_row` is an archetype index row in range of the current archetype, because if it was not, then the if above would have been executed. - let archetype_entity = - unsafe { self.archetype_entities.get_unchecked(self.current_row) }; + let archetype_entity = unsafe { + self.archetype_entities + .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(), @@ -2550,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 c1744cbf24..94772cc9b9 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -107,7 +107,7 @@ mod tests { use crate::{ archetype::Archetype, component::{Component, ComponentId, Components, Tick}, - prelude::{AnyOf, Changed, Entity, Or, QueryState, Res, ResMut, Resource, With, Without}, + prelude::{AnyOf, Changed, Entity, Or, QueryState, Resource, With, Without}, query::{ ArchetypeFilter, FilteredAccess, Has, QueryCombinationIter, QueryData, ReadOnlyQueryData, WorldQuery, @@ -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)] @@ -717,7 +717,7 @@ mod tests { } let mut system = IntoSystem::into_system(system); system.initialize(&mut world); - system.run((), &mut world); + system.run((), &mut world).unwrap(); } { fn system(has_a: Query>, mut b_query: Query<&mut B>) { @@ -728,7 +728,7 @@ mod tests { } let mut system = IntoSystem::into_system(system); system.initialize(&mut world); - system.run((), &mut world); + system.run((), &mut world).unwrap(); } { fn system(query: Query<(Option<&A>, &B)>) { @@ -741,7 +741,7 @@ mod tests { } let mut system = IntoSystem::into_system(system); system.initialize(&mut world); - system.run((), &mut world); + system.run((), &mut world).unwrap(); } } @@ -818,16 +818,15 @@ mod tests { /// SAFETY: /// `update_component_access` adds resource read access for `R`. - /// `update_archetype_component_access` does nothing, as this accesses no components. unsafe impl WorldQuery for ReadsRData { type Fetch<'w> = (); type State = ComponentId; 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> { @@ -836,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, ) { } @@ -883,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> { } } @@ -904,28 +907,4 @@ mod tests { fn system(_q1: Query>, _q2: Query>) {} assert_is_system(system); } - - #[test] - fn read_res_sets_archetype_component_access() { - let mut world = World::new(); - - fn read_query(_q: Query>) {} - let mut read_query = IntoSystem::into_system(read_query); - read_query.initialize(&mut world); - - fn read_res(_r: Res) {} - let mut read_res = IntoSystem::into_system(read_res); - read_res.initialize(&mut world); - - fn write_res(_r: ResMut) {} - let mut write_res = IntoSystem::into_system(write_res); - write_res.initialize(&mut world); - - assert!(read_query - .archetype_component_access() - .is_compatible(read_res.archetype_component_access())); - assert!(!read_query - .archetype_component_access() - .is_compatible(write_res.archetype_component_access())); - } } diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index 6683238aa3..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)); } @@ -62,7 +62,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { /// query.par_iter().for_each_init(|| queue.borrow_local_mut(),|local_queue, item| { /// **local_queue += 1; /// }); - /// + /// /// // collect value from every thread /// let entity_count: usize = queue.iter_mut().map(|v| *v).sum(); /// } @@ -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| { @@ -130,7 +130,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { } #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - fn get_batch_size(&self, thread_count: usize) -> usize { + fn get_batch_size(&self, thread_count: usize) -> u32 { let max_items = || { let id_iter = self.state.matched_storage_ids.iter(); if self.state.is_dense { @@ -147,10 +147,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { .map(|id| unsafe { archetypes[id.archetype_id].len() }) .max() } + .map(|v| v as usize) .unwrap_or(0) }; self.batching_strategy - .calc_batch_size(max_items, thread_count) + .calc_batch_size(max_items, thread_count) as u32 } } @@ -189,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)); } @@ -232,7 +233,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// query.par_iter_many(&entities).for_each_init(|| queue.borrow_local_mut(),|local_queue, item| { /// **local_queue += some_expensive_operation(item); /// }); - /// + /// /// // collect value from every thread /// let final_value: usize = queue.iter_mut().map(|v| *v).sum(); /// } @@ -246,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| { @@ -301,9 +302,9 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> } #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - fn get_batch_size(&self, thread_count: usize) -> usize { + fn get_batch_size(&self, thread_count: usize) -> u32 { self.batching_strategy - .calc_batch_size(|| self.entity_list.len(), thread_count) + .calc_batch_size(|| self.entity_list.len(), thread_count) as u32 } } @@ -344,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)); } @@ -387,7 +388,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// query.par_iter_many_unique(&entities).for_each_init(|| queue.borrow_local_mut(),|local_queue, item| { /// **local_queue += some_expensive_operation(item); /// }); - /// + /// /// // collect value from every thread /// let final_value: usize = queue.iter_mut().map(|v| *v).sum(); /// } @@ -401,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| { @@ -456,8 +457,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> } #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - fn get_batch_size(&self, thread_count: usize) -> usize { + fn get_batch_size(&self, thread_count: usize) -> u32 { self.batching_strategy - .calc_batch_size(|| self.entity_list.len(), thread_count) + .calc_batch_size(|| self.entity_list.len(), thread_count) as u32 } } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index cae8e592e7..00d8b6f970 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1,10 +1,10 @@ use crate::{ - archetype::{Archetype, ArchetypeComponentId, ArchetypeGeneration, ArchetypeId}, + archetype::{Archetype, ArchetypeGeneration, ArchetypeId}, component::{ComponentId, Tick}, entity::{Entity, EntityEquivalent, EntitySet, UniqueEntityArray}, entity_disabling::DefaultQueryFilters, prelude::FromWorld, - query::{Access, FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, WorldQuery}, + query::{FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, WorldQuery}, storage::{SparseSetIndex, TableId}, system::Query, world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId}, @@ -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; @@ -21,8 +22,8 @@ use log::warn; use tracing::Span; use super::{ - ComponentAccessKind, NopWorldQuery, QueryBuilder, QueryData, QueryEntityError, QueryFilter, - QueryManyIter, QueryManyUniqueIter, QuerySingleError, ROQueryItem, ReadOnlyQueryData, + NopWorldQuery, QueryBuilder, QueryData, QueryEntityError, QueryFilter, QueryManyIter, + QueryManyUniqueIter, QuerySingleError, ROQueryItem, ReadOnlyQueryData, }; /// An ID for either a table or an archetype. Used for Query iteration. @@ -175,40 +176,6 @@ impl QueryState { Some(state) } - /// Identical to `new`, but it populates the provided `access` with the matched results. - pub(crate) fn new_with_access( - world: &mut World, - access: &mut Access, - ) -> Self { - let mut state = Self::new_uninitialized(world); - for archetype in world.archetypes.iter() { - // SAFETY: The state was just initialized from the `world` above, and the archetypes being added - // come directly from the same world. - unsafe { - if state.new_archetype_internal(archetype) { - state.update_archetype_component_access(archetype, access); - } - } - } - state.archetype_generation = world.archetypes.generation(); - - // Resource access is not part of any archetype and must be handled separately - if state.component_access.access().has_read_all_resources() { - access.read_all_resources(); - } else { - for component_id in state.component_access.access().resource_reads() { - access.add_resource_read(world.initialize_resource_internal(component_id).id()); - } - } - - debug_assert!( - !state.component_access.access().has_any_resource_write(), - "Mutable resource access in queries is not allowed" - ); - - state - } - /// Creates a new [`QueryState`] but does not populate it with the matched results from the World yet /// /// `new_archetype` and its variants must be called on all of the World's archetypes before the @@ -546,7 +513,7 @@ impl QueryState { // SAFETY: The validate_world call ensures that the world is the same the QueryState // was initialized from. unsafe { - self.new_archetype_internal(archetype); + self.new_archetype(archetype); } } } else { @@ -581,7 +548,7 @@ impl QueryState { // SAFETY: The validate_world call ensures that the world is the same the QueryState // was initialized from. unsafe { - self.new_archetype_internal(archetype); + self.new_archetype(archetype); } } } @@ -613,32 +580,9 @@ impl QueryState { /// Update the current [`QueryState`] with information from the provided [`Archetype`] /// (if applicable, i.e. if the archetype has any intersecting [`ComponentId`] with the current [`QueryState`]). /// - /// The passed in `access` will be updated with any new accesses introduced by the new archetype. - /// /// # Safety /// `archetype` must be from the `World` this state was initialized from. - pub unsafe fn new_archetype( - &mut self, - archetype: &Archetype, - access: &mut Access, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from. - let matches = unsafe { self.new_archetype_internal(archetype) }; - if matches { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from. - unsafe { self.update_archetype_component_access(archetype, access) }; - } - } - - /// Process the given [`Archetype`] to update internal metadata about the [`Table`](crate::storage::Table)s - /// and [`Archetype`]s that are matched by this query. - /// - /// Returns `true` if the given `archetype` matches the query. Otherwise, returns `false`. - /// If there is no match, then there is no need to update the query's [`FilteredAccess`]. - /// - /// # Safety - /// `archetype` must be from the `World` this state was initialized from. - unsafe fn new_archetype_internal(&mut self, archetype: &Archetype) -> bool { + pub unsafe fn new_archetype(&mut self, archetype: &Archetype) { if D::matches_component_set(&self.fetch_state, &|id| archetype.contains(id)) && F::matches_component_set(&self.filter_state, &|id| archetype.contains(id)) && self.matches_component_set(&|id| archetype.contains(id)) @@ -661,9 +605,6 @@ impl QueryState { }); } } - true - } else { - false } } @@ -680,57 +621,6 @@ impl QueryState { }) } - /// For the given `archetype`, adds any component accessed used by this query's underlying [`FilteredAccess`] to `access`. - /// - /// The passed in `access` will be updated with any new accesses introduced by the new archetype. - /// - /// # Safety - /// `archetype` must be from the `World` this state was initialized from. - pub unsafe fn update_archetype_component_access( - &mut self, - archetype: &Archetype, - access: &mut Access, - ) { - // As a fast path, we can iterate directly over the components involved - // if the `access` is finite. - if let Ok(iter) = self.component_access.access.try_iter_component_access() { - iter.for_each(|component_access| { - if let Some(id) = archetype.get_archetype_component_id(*component_access.index()) { - match component_access { - ComponentAccessKind::Archetypal(_) => {} - ComponentAccessKind::Shared(_) => { - access.add_component_read(id); - } - ComponentAccessKind::Exclusive(_) => { - access.add_component_write(id); - } - } - } - }); - - return; - } - - for (component_id, archetype_component_id) in - archetype.components_with_archetype_component_id() - { - if self - .component_access - .access - .has_component_read(component_id) - { - access.add_component_read(archetype_component_id); - } - if self - .component_access - .access - .has_component_write(component_id) - { - access.add_component_write(archetype_component_id); - } - } - } - /// Use this to transform a [`QueryState`] into a more generic [`QueryState`]. /// This can be useful for passing to another function that might take the more general form. /// See [`Query::transmute_lens`](crate::system::Query::transmute_lens) for more details. @@ -783,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 { @@ -902,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 { @@ -956,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) } @@ -1003,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) } @@ -1041,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) } @@ -1053,7 +947,7 @@ impl QueryState { &mut self, world: &'w mut World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_mut(world).get_inner(entity) } @@ -1100,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) } @@ -1145,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) } @@ -1167,7 +1061,7 @@ impl QueryState { &self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_manual(world).get_inner(entity) } @@ -1184,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() @@ -1279,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. @@ -1498,16 +1398,16 @@ 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: usize, + batch_size: u32, func: FN, 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: @@ -1516,7 +1416,7 @@ impl QueryState { use arrayvec::ArrayVec; bevy_tasks::ComputeTaskPool::get().scope(|scope| { - // SAFETY: We only access table data that has been registered in `self.archetype_component_access`. + // SAFETY: We only access table data that has been registered in `self.component_access`. let tables = unsafe { &world.storages().tables }; let archetypes = world.archetypes(); let mut batch_queue = ArrayVec::new(); @@ -1545,7 +1445,7 @@ impl QueryState { // submit single storage larger than batch_size let submit_single = |count, storage_id: StorageId| { - for offset in (0..count).step_by(batch_size) { + for offset in (0..count).step_by(batch_size as usize) { let mut func = func.clone(); let init_accum = init_accum.clone(); let len = batch_size.min(count - offset); @@ -1561,7 +1461,7 @@ impl QueryState { } }; - let storage_entity_count = |storage_id: StorageId| -> usize { + let storage_entity_count = |storage_id: StorageId| -> u32 { if self.is_dense { tables[storage_id.table_id].entity_count() } else { @@ -1612,17 +1512,17 @@ 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, - batch_size: usize, + batch_size: u32, mut func: FN, 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, { @@ -1631,7 +1531,7 @@ impl QueryState { // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual bevy_tasks::ComputeTaskPool::get().scope(|scope| { - let chunks = entity_list.chunks_exact(batch_size); + let chunks = entity_list.chunks_exact(batch_size as usize); let remainder = chunks.remainder(); for batch in chunks { @@ -1675,17 +1575,17 @@ 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], - batch_size: usize, + batch_size: u32, mut func: FN, 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, { @@ -1694,7 +1594,7 @@ impl QueryState { // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual bevy_tasks::ComputeTaskPool::get().scope(|scope| { - let chunks = entity_list.chunks_exact(batch_size); + let chunks = entity_list.chunks_exact(batch_size as usize); let remainder = chunks.remainder(); for batch in chunks { @@ -1797,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() } @@ -1814,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() } @@ -1831,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() } @@ -1853,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. @@ -1998,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::(); @@ -2012,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)); @@ -2024,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)); @@ -2038,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::(); @@ -2106,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::(); @@ -2118,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. @@ -2227,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::(); @@ -2239,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); @@ -2253,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/entity_commands.rs b/crates/bevy_ecs/src/reflect/entity_commands.rs index 20c5e16c6d..6b5fad540e 100644 --- a/crates/bevy_ecs/src/reflect/entity_commands.rs +++ b/crates/bevy_ecs/src/reflect/entity_commands.rs @@ -102,7 +102,7 @@ pub trait ReflectCommandExt { component: Box, ) -> &mut Self; - /// Removes from the entity the component or bundle with the given type name registered in [`AppTypeRegistry`]. + /// Removes from the entity the component or bundle with the given type path registered in [`AppTypeRegistry`]. /// /// If the type is a bundle, it will remove any components in that bundle regardless if the entity /// contains all the components. @@ -159,12 +159,12 @@ pub trait ReflectCommandExt { /// .remove_reflect(prefab.data.reflect_type_path().to_owned()); /// } /// ``` - fn remove_reflect(&mut self, component_type_name: impl Into>) -> &mut Self; + fn remove_reflect(&mut self, component_type_path: impl Into>) -> &mut Self; /// Same as [`remove_reflect`](ReflectCommandExt::remove_reflect), but using the `T` resource as type registry instead of /// `AppTypeRegistry`. fn remove_reflect_with_registry>( &mut self, - component_type_name: impl Into>, + component_type_path: impl Into>, ) -> &mut Self; } @@ -263,7 +263,7 @@ impl<'w> EntityWorldMut<'w> { self } - /// Removes from the entity the component or bundle with the given type name registered in [`AppTypeRegistry`]. + /// Removes from the entity the component or bundle with the given type path registered in [`AppTypeRegistry`]. /// /// If the type is a bundle, it will remove any components in that bundle regardless if the entity /// contains all the components. @@ -349,7 +349,7 @@ fn insert_reflect_with_registry_ref( .expect("component should represent a type."); let type_path = type_info.type_path(); let Ok(mut entity) = world.get_entity_mut(entity) else { - panic!("error[B0003]: Could not insert a reflected component (of type {type_path}) for entity {entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", + panic!("error[B0003]: Could not insert a reflected component (of type {type_path}) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003", world.entities().entity_does_not_exist_error_details(entity)); }; let Some(type_registration) = type_registry.get(type_info.type_id()) else { 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 9a2a2a2d5a..82b39e04e5 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -6,15 +6,16 @@ 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::*; use crate::{ - component::{Component, HookContext, Mutable}, + component::{Component, Mutable}, entity::{ComponentCloneCtx, Entity, SourceComponent}, - error::{ignore, CommandWithEntity, HandleError}, - system::entity_command::{self}, + error::CommandWithEntity, + lifecycle::HookContext, world::{DeferredWorld, EntityWorldMut}, }; use log::warn; @@ -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,28 +120,30 @@ 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; } - if let Ok(mut target_entity_mut) = world.get_entity_mut(target_entity) { - if let Some(mut relationship_target) = - target_entity_mut.get_mut::() - { - relationship_target.collection_mut_risky().add(entity); - } else { - let mut target = ::with_capacity(1); - target.collection_mut_risky().add(entity); - world.commands().entity(target_entity).insert(target); - } + if let Ok(mut entity_commands) = world.commands().get_entity(target_entity) { + // Deferring is necessary for batch mode + entity_commands + .entry::() + .and_modify(move |mut relationship_target| { + relationship_target.collection_mut_risky().add(entity); + }) + .or_insert_with(move || { + let mut target = Self::RelationshipTarget::with_capacity(1); + target.collection_mut_risky().add(entity); + target + }); } else { 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::(); } @@ -158,19 +175,21 @@ pub trait Relationship: Component + Sized { { relationship_target.collection_mut_risky().remove(entity); if relationship_target.len() == 0 { - if let Ok(mut entity) = world.commands().get_entity(target_entity) { + let command = |mut entity: EntityWorldMut| { // this "remove" operation must check emptiness because in the event that an identical // relationship is inserted on top, this despawn would result in the removal of that identical // relationship ... not what we want! - entity.queue(|mut entity: EntityWorldMut| { - if entity - .get::() - .is_some_and(RelationshipTarget::is_empty) - { - entity.remove::(); - } - }); - } + if entity + .get::() + .is_some_and(RelationshipTarget::is_empty) + { + entity.remove::(); + } + }; + + world + .commands() + .queue_silenced(command.with_entity(target_entity)); } } } @@ -221,50 +240,24 @@ pub trait RelationshipTarget: Component + Sized { /// The `on_replace` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection. // note: think of this as "on_drop" - fn on_replace(mut world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + fn on_replace(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { - if entities.get(source_entity).is_ok() { - commands.queue( - entity_command::remove::() - .with_entity(source_entity) - .handle_error_with(ignore), - ); - } else { - warn!( - "{}Tried to despawn non-existent entity {}", - caller - .map(|location| format!("{location}: ")) - .unwrap_or_default(), - source_entity - ); - } + commands + .entity(source_entity) + .try_remove::(); } } /// The `on_despawn` component hook that despawns entities stored in an entity's [`RelationshipTarget`] when /// that entity is despawned. // note: think of this as "on_drop" - fn on_despawn(mut world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + fn on_despawn(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { - if entities.get(source_entity).is_ok() { - commands.queue( - entity_command::despawn() - .with_entity(source_entity) - .handle_error_with(ignore), - ); - } else { - warn!( - "{}Tried to despawn non-existent entity {}", - caller - .map(|location| format!("{location}: ")) - .unwrap_or_default(), - source_entity - ); - } + commands.entity(source_entity).try_despawn(); } } @@ -332,6 +325,7 @@ pub enum RelationshipHookMode { #[cfg(test)] mod tests { + use crate::prelude::{ChildOf, Children}; use crate::world::World; use crate::{component::Component, entity::Entity}; use alloc::vec::Vec; @@ -424,4 +418,91 @@ mod tests { // No assert necessary, looking to make sure compilation works with the macros } + + #[test] + fn parent_child_relationship_with_custom_relationship() { + #[derive(Component)] + #[relationship(relationship_target = RelTarget)] + struct Rel(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Rel)] + struct RelTarget(Entity); + + let mut world = World::new(); + + // Rel on Parent + // Despawn Parent + let mut commands = world.commands(); + let child = commands.spawn_empty().id(); + let parent = commands.spawn(Rel(child)).add_child(child).id(); + commands.entity(parent).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(world.get_entity(parent).is_err()); + + // Rel on Parent + // Despawn Child + let mut commands = world.commands(); + let child = commands.spawn_empty().id(); + let parent = commands.spawn(Rel(child)).add_child(child).id(); + commands.entity(child).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(!world.entity(parent).contains::()); + + // Rel on Child + // Despawn Parent + let mut commands = world.commands(); + let parent = commands.spawn_empty().id(); + let child = commands.spawn((ChildOf(parent), Rel(parent))).id(); + commands.entity(parent).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(world.get_entity(parent).is_err()); + + // Rel on Child + // Despawn Child + let mut commands = world.commands(); + let parent = commands.spawn_empty().id(); + let child = commands.spawn((ChildOf(parent), Rel(parent))).id(); + commands.entity(child).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(!world.entity(parent).contains::()); + } + + #[test] + fn spawn_batch_with_relationship() { + let mut world = World::new(); + let parent = world.spawn_empty().id(); + let children = world + .spawn_batch((0..10).map(|_| ChildOf(parent))) + .collect::>(); + + for &child in &children { + assert!(world + .get::(child) + .is_some_and(|child_of| child_of.parent() == parent)); + } + assert!(world + .get::(parent) + .is_some_and(|children| children.len() == 10)); + } + + #[test] + fn insert_batch_with_relationship() { + let mut world = World::new(); + let parent = world.spawn_empty().id(); + let child = world.spawn_empty().id(); + world.insert_batch([(child, ChildOf(parent))]); + world.flush(); + + assert!(world.get::(child).is_some()); + assert!(world.get::(parent).is_some()); + } } diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 5a23214463..8bae76a84e 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -1,11 +1,12 @@ use crate::{ bundle::Bundle, entity::{hash_set::EntityHashSet, Entity}, + prelude::Children, relationship::{ 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}; @@ -41,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 @@ -97,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") @@ -138,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 } @@ -238,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| { @@ -251,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 } @@ -302,6 +311,15 @@ impl<'w> EntityWorldMut<'w> { self } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityWorldMut::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::(); + self + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -350,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> { @@ -467,6 +519,14 @@ impl<'a> EntityCommands<'a> { }) } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityCommands::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::() + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -650,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..668118003b 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. @@ -687,36 +687,40 @@ mod tests { #[test] fn entity_index_map() { - #[derive(Component)] - #[relationship(relationship_target = RelTarget)] - struct Rel(Entity); + for add_before in [false, true] { + #[derive(Component)] + #[relationship(relationship_target = RelTarget)] + struct Rel(Entity); - #[derive(Component)] - #[relationship_target(relationship = Rel, linked_spawn)] - struct RelTarget(EntityHashSet); + #[derive(Component)] + #[relationship_target(relationship = Rel, linked_spawn)] + struct RelTarget(Vec); - let mut world = World::new(); - let a = world.spawn_empty().id(); - let b = world.spawn_empty().id(); - let c = world.spawn_empty().id(); + let mut world = World::new(); + if add_before { + let _ = world.spawn_empty().id(); + } + let a = world.spawn_empty().id(); + let b = world.spawn_empty().id(); + let c = world.spawn_empty().id(); + let d = world.spawn_empty().id(); - let d = world.spawn_empty().id(); + world.entity_mut(a).add_related::(&[b, c, d]); - world.entity_mut(a).add_related::(&[b, c, d]); + let rel_target = world.get::(a).unwrap(); + let collection = rel_target.collection(); - let rel_target = world.get::(a).unwrap(); - let collection = rel_target.collection(); + // Insertions should maintain ordering + assert!(collection.iter().eq([b, c, d])); - // Insertions should maintain ordering - assert!(collection.iter().eq(&[d, c, b])); + world.entity_mut(c).despawn(); - world.entity_mut(c).despawn(); + let rel_target = world.get::(a).unwrap(); + let collection = rel_target.collection(); - let rel_target = world.get::(a).unwrap(); - let collection = rel_target.collection(); - - // Removals should maintain ordering - assert!(collection.iter().eq(&[d, b])); + // Removals should maintain ordering + assert!(collection.iter().eq([b, d])); + } } #[test] diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs deleted file mode 100644 index 64cc63a7ce..0000000000 --- a/crates/bevy_ecs/src/removal_detection.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Alerting events when a component is removed from an entity. - -use crate::{ - component::{Component, ComponentId, ComponentIdFor, Tick}, - entity::Entity, - event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, - prelude::Local, - storage::SparseSet, - system::{ReadOnlySystemParam, SystemMeta, SystemParam}, - world::{unsafe_world_cell::UnsafeWorldCell, World}, -}; - -use derive_more::derive::Into; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; -use core::{ - fmt::Debug, - iter, - marker::PhantomData, - ops::{Deref, DerefMut}, - option, -}; - -/// Wrapper around [`Entity`] for [`RemovedComponents`]. -/// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, Debug, Clone, Into)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] -pub struct RemovedComponentEntity(Entity); - -/// Wrapper around a [`EventCursor`] so that we -/// can differentiate events between components. -#[derive(Debug)] -pub struct RemovedComponentReader -where - T: Component, -{ - reader: EventCursor, - marker: PhantomData, -} - -impl Default for RemovedComponentReader { - fn default() -> Self { - Self { - reader: Default::default(), - marker: PhantomData, - } - } -} - -impl Deref for RemovedComponentReader { - type Target = EventCursor; - fn deref(&self) -> &Self::Target { - &self.reader - } -} - -impl DerefMut for RemovedComponentReader { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.reader - } -} - -/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. -#[derive(Default, Debug)] -pub struct RemovedComponentEvents { - event_sets: SparseSet>, -} - -impl RemovedComponentEvents { - /// Creates an empty storage buffer for component removal events. - pub fn new() -> Self { - Self::default() - } - - /// For each type of component, swaps the event buffers and clears the oldest event buffer. - /// In general, this should be called once per frame/update. - pub fn update(&mut self) { - for (_component_id, events) in self.event_sets.iter_mut() { - events.update(); - } - } - - /// Returns an iterator over components and their entity events. - pub fn iter(&self) -> impl Iterator)> { - self.event_sets.iter() - } - - /// Gets the event storage for a given component. - pub fn get( - &self, - component_id: impl Into, - ) -> Option<&Events> { - self.event_sets.get(component_id.into()) - } - - /// Sends a removal event for the specified component. - pub fn send(&mut self, component_id: impl Into, entity: Entity) { - self.event_sets - .get_or_insert_with(component_id.into(), Default::default) - .send(RemovedComponentEntity(entity)); - } -} - -/// A [`SystemParam`] that yields entities that had their `T` [`Component`] -/// removed or have been despawned with it. -/// -/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). -/// -/// Note that this does not allow you to see which data existed before removal. -/// If you need this, you will need to track the component data value on your own, -/// using a regularly scheduled system that requests `Query<(Entity, &T), Changed>` -/// and stores the data somewhere safe to later cross-reference. -/// -/// If you are using `bevy_ecs` as a standalone crate, -/// note that the `RemovedComponents` list will not be automatically cleared for you, -/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). -/// -/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is -/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. -/// For the main world, this is delayed until after all `SubApp`s have run. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use bevy_ecs::component::Component; -/// # use bevy_ecs::system::IntoSystem; -/// # use bevy_ecs::removal_detection::RemovedComponents; -/// # -/// # #[derive(Component)] -/// # struct MyComponent; -/// fn react_on_removal(mut removed: RemovedComponents) { -/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); -/// } -/// # bevy_ecs::system::assert_is_system(react_on_removal); -/// ``` -#[derive(SystemParam)] -pub struct RemovedComponents<'w, 's, T: Component> { - component_id: ComponentIdFor<'s, T>, - reader: Local<'s, RemovedComponentReader>, - event_sets: &'w RemovedComponentEvents, -} - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIter<'a> = iter::Map< - iter::Flatten>>>, - fn(RemovedComponentEntity) -> Entity, ->; - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIterWithId<'a> = iter::Map< - iter::Flatten>>, - fn( - (&RemovedComponentEntity, EventId), - ) -> (Entity, EventId), ->; - -fn map_id_events( - (entity, id): (&RemovedComponentEntity, EventId), -) -> (Entity, EventId) { - (entity.clone().into(), id) -} - -// For all practical purposes, the api surface of `RemovedComponents` -// should be similar to `EventReader` to reduce confusion. -impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { - /// Fetch underlying [`EventCursor`]. - pub fn reader(&self) -> &EventCursor { - &self.reader - } - - /// Fetch underlying [`EventCursor`] mutably. - pub fn reader_mut(&mut self) -> &mut EventCursor { - &mut self.reader - } - - /// Fetch underlying [`Events`]. - pub fn events(&self) -> Option<&Events> { - self.event_sets.get(self.component_id.get()) - } - - /// Destructures to get a mutable reference to the `EventCursor` - /// and a reference to `Events`. - /// - /// This is necessary since Rust can't detect destructuring through methods and most - /// usecases of the reader uses the `Events` as well. - pub fn reader_mut_with_events( - &mut self, - ) -> Option<( - &mut RemovedComponentReader, - &Events, - )> { - self.event_sets - .get(self.component_id.get()) - .map(|events| (&mut *self.reader, events)) - } - - /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the - /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events - /// that happened before now. - pub fn read(&mut self) -> RemovedIter<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read(events).cloned()) - .into_iter() - .flatten() - .map(RemovedComponentEntity::into) - } - - /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. - pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read_with_id(events)) - .into_iter() - .flatten() - .map(map_id_events) - } - - /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. - pub fn len(&self) -> usize { - self.events() - .map(|events| self.reader.len(events)) - .unwrap_or(0) - } - - /// Returns `true` if there are no events available to read. - pub fn is_empty(&self) -> bool { - self.events() - .is_none_or(|events| self.reader.is_empty(events)) - } - - /// Consumes all available events. - /// - /// This means these events will not appear in calls to [`RemovedComponents::read()`] or - /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. - pub fn clear(&mut self) { - if let Some((reader, events)) = self.reader_mut_with_events() { - reader.clear(events); - } - } -} - -// SAFETY: Only reads World removed component events -unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} - -// SAFETY: no component value access. -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 {} - - #[inline] - unsafe fn get_param<'w, 's>( - _state: &'s mut Self::State, - _system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - _change_tick: Tick, - ) -> Self::Item<'w, 's> { - world.removed_components() - } -} diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index c3f7805631..7da4f31113 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -54,7 +54,7 @@ pub use bevy_ecs_macros::Resource; /// ``` /// # use std::cell::RefCell; /// # use bevy_ecs::resource::Resource; -/// use bevy_utils::synccell::SyncCell; +/// use bevy_platform::cell::SyncCell; /// /// #[derive(Resource)] /// struct ActuallySync { @@ -66,7 +66,7 @@ pub use bevy_ecs_macros::Resource; /// [`World`]: crate::world::World /// [`Res`]: crate::system::Res /// [`ResMut`]: crate::system::ResMut -/// [`SyncCell`]: bevy_utils::synccell::SyncCell +/// [`SyncCell`]: bevy_platform::cell::SyncCell #[diagnostic::on_unimplemented( message = "`{Self}` is not a `Resource`", label = "invalid `Resource`", 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..524b198358 100644 --- a/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs +++ b/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs @@ -2,8 +2,11 @@ use alloc::{boxed::Box, collections::BTreeSet, vec::Vec}; use bevy_platform::collections::HashMap; -use crate::system::IntoSystem; -use crate::world::World; +use crate::{ + schedule::{SystemKey, SystemSetKey}, + system::IntoSystem, + world::World, +}; use super::{ is_apply_deferred, ApplyDeferred, DiGraph, Direction, NodeId, ReportCycles, ScheduleBuildError, @@ -36,29 +39,26 @@ impl AutoInsertApplyDeferredPass { self.auto_sync_node_ids .get(&distance) .copied() - .or_else(|| { - let node_id = self.add_auto_sync(graph); + .unwrap_or_else(|| { + let node_id = NodeId::System(self.add_auto_sync(graph)); self.auto_sync_node_ids.insert(distance, node_id); - Some(node_id) + node_id }) - .unwrap() } /// add an [`ApplyDeferred`] system with no config - fn add_auto_sync(&mut self, graph: &mut ScheduleGraph) -> NodeId { - let id = NodeId::System(graph.systems.len()); - - graph + fn add_auto_sync(&mut self, graph: &mut ScheduleGraph) -> SystemKey { + let key = graph .systems - .push(SystemNode::new(Box::new(IntoSystem::into_system( + .insert(SystemNode::new(Box::new(IntoSystem::into_system( ApplyDeferred, )))); - graph.system_conditions.push(Vec::new()); + graph.system_conditions.insert(key, Vec::new()); // ignore ambiguities with auto sync points // They aren't under user control, so no one should know or care. - graph.ambiguous_with_all.insert(id); + graph.ambiguous_with_all.insert(NodeId::System(key)); - id + key } } @@ -80,39 +80,45 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { let mut sync_point_graph = dependency_flattened.clone(); let topo = graph.topsort_graph(dependency_flattened, ReportCycles::Dependency)?; - fn set_has_conditions(graph: &ScheduleGraph, node: NodeId) -> bool { - !graph.set_conditions_at(node).is_empty() + fn set_has_conditions(graph: &ScheduleGraph, set: SystemSetKey) -> bool { + !graph.set_conditions_at(set).is_empty() || graph .hierarchy() .graph() - .edges_directed(node, Direction::Incoming) - .any(|(parent, _)| set_has_conditions(graph, parent)) + .edges_directed(NodeId::Set(set), Direction::Incoming) + .any(|(parent, _)| { + parent + .as_set() + .is_some_and(|p| set_has_conditions(graph, p)) + }) } - fn system_has_conditions(graph: &ScheduleGraph, node: NodeId) -> bool { - assert!(node.is_system()); - !graph.system_conditions[node.index()].is_empty() + fn system_has_conditions(graph: &ScheduleGraph, key: SystemKey) -> bool { + !graph.system_conditions[key].is_empty() || graph .hierarchy() .graph() - .edges_directed(node, Direction::Incoming) - .any(|(parent, _)| set_has_conditions(graph, parent)) + .edges_directed(NodeId::System(key), Direction::Incoming) + .any(|(parent, _)| { + parent + .as_set() + .is_some_and(|p| set_has_conditions(graph, p)) + }) } - 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()) + let mut system_has_conditions_cache = HashMap::::default(); + let mut is_valid_explicit_sync_point = |key: SystemKey| { + is_apply_deferred(&graph.systems[key].get().unwrap().system) && !*system_has_conditions_cache - .entry(index) - .or_insert_with(|| system_has_conditions(graph, system)) + .entry(key) + .or_insert_with(|| system_has_conditions(graph, key)) }; // Calculate the distance for each node. // The "distance" is the number of sync points between a node and the beginning of the graph. // Also store if a preceding edge would have added a sync point but was ignored to add it at // a later edge that is not ignored. - let mut distances_and_pending_sync: HashMap = + let mut distances_and_pending_sync: HashMap = HashMap::with_capacity_and_hasher(topo.len(), Default::default()); // Keep track of any explicit sync nodes for a specific distance. @@ -120,17 +126,21 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { // Determine the distance for every node and collect the explicit sync points. for node in &topo { + let &NodeId::System(key) = node else { + panic!("Encountered a non-system node in the flattened dependency graph: {node:?}"); + }; + let (node_distance, mut node_needs_sync) = distances_and_pending_sync - .get(&node.index()) + .get(&key) .copied() .unwrap_or_default(); - if is_valid_explicit_sync_point(*node) { + if is_valid_explicit_sync_point(key) { // The distance of this sync point does not change anymore as the iteration order // makes sure that this node is no unvisited target of another node. // Because of this, the sync point can be stored for this distance to be reused as // automatically added sync points later. - distance_to_explicit_sync_node.insert(node_distance, *node); + distance_to_explicit_sync_node.insert(node_distance, NodeId::System(key)); // This node just did a sync, so the only reason to do another sync is if one was // explicitly scheduled afterwards. @@ -138,18 +148,22 @@ 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[key].get().unwrap().system.has_deferred(); } for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) { - let (target_distance, target_pending_sync) = distances_and_pending_sync - .entry(target.index()) - .or_default(); + let NodeId::System(target) = target else { + panic!("Encountered a non-system node in the flattened dependency graph: {target:?}"); + }; + let (target_distance, target_pending_sync) = + distances_and_pending_sync.entry(target).or_default(); let mut edge_needs_sync = node_needs_sync; if node_needs_sync - && !graph.systems[target.index()].get().unwrap().is_exclusive() - && self.no_sync_edges.contains(&(*node, target)) + && !graph.systems[target].get().unwrap().system.is_exclusive() + && self + .no_sync_edges + .contains(&(*node, NodeId::System(target))) { // The node has deferred params to apply, but this edge is ignoring sync points. // Mark the target as 'delaying' those commands to a future edge and the current @@ -174,14 +188,20 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { // Find any edges which have a different number of sync points between them and make sure // there is a sync point between them. for node in &topo { + let &NodeId::System(key) = node else { + panic!("Encountered a non-system node in the flattened dependency graph: {node:?}"); + }; let (node_distance, _) = distances_and_pending_sync - .get(&node.index()) + .get(&key) .copied() .unwrap_or_default(); for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) { + let NodeId::System(target) = target else { + panic!("Encountered a non-system node in the flattened dependency graph: {target:?}"); + }; let (target_distance, _) = distances_and_pending_sync - .get(&target.index()) + .get(&target) .copied() .unwrap_or_default(); @@ -190,7 +210,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { continue; } - if is_apply_deferred(graph.systems[target.index()].get().unwrap()) { + if is_apply_deferred(&graph.systems[target].get().unwrap().system) { // We don't need to insert a sync point since ApplyDeferred is a sync point // already! continue; @@ -202,10 +222,10 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { .unwrap_or_else(|| self.get_sync_point(graph, target_distance)); sync_point_graph.add_edge(*node, sync_point); - sync_point_graph.add_edge(sync_point, target); + sync_point_graph.add_edge(sync_point, NodeId::System(target)); // The edge without the sync point is now redundant. - sync_point_graph.remove_edge(*node, target); + sync_point_graph.remove_edge(*node, NodeId::System(target)); } } @@ -215,34 +235,39 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { fn collapse_set( &mut self, - set: NodeId, - systems: &[NodeId], + set: SystemSetKey, + systems: &[SystemKey], dependency_flattened: &DiGraph, ) -> impl Iterator { if systems.is_empty() { // collapse dependencies for empty sets - for a in dependency_flattened.neighbors_directed(set, Direction::Incoming) { - for b in dependency_flattened.neighbors_directed(set, Direction::Outgoing) { - if self.no_sync_edges.contains(&(a, set)) - && self.no_sync_edges.contains(&(set, b)) + for a in dependency_flattened.neighbors_directed(NodeId::Set(set), Direction::Incoming) + { + for b in + dependency_flattened.neighbors_directed(NodeId::Set(set), Direction::Outgoing) + { + if self.no_sync_edges.contains(&(a, NodeId::Set(set))) + && self.no_sync_edges.contains(&(NodeId::Set(set), b)) { self.no_sync_edges.insert((a, b)); } } } } else { - for a in dependency_flattened.neighbors_directed(set, Direction::Incoming) { + for a in dependency_flattened.neighbors_directed(NodeId::Set(set), Direction::Incoming) + { for &sys in systems { - if self.no_sync_edges.contains(&(a, set)) { - self.no_sync_edges.insert((a, sys)); + if self.no_sync_edges.contains(&(a, NodeId::Set(set))) { + self.no_sync_edges.insert((a, NodeId::System(sys))); } } } - for b in dependency_flattened.neighbors_directed(set, Direction::Outgoing) { + for b in dependency_flattened.neighbors_directed(NodeId::Set(set), Direction::Outgoing) + { for &sys in systems { - if self.no_sync_edges.contains(&(set, b)) { - self.no_sync_edges.insert((sys, b)); + if self.no_sync_edges.contains(&(NodeId::Set(set), b)) { + self.no_sync_edges.insert((NodeId::System(sys), b)); } } } diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index a85a8c6fa4..2cd71db9b4 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1,9 +1,10 @@ -use alloc::{borrow::Cow, boxed::Box, format}; +use alloc::{boxed::Box, format}; +use bevy_utils::prelude::DebugName; use core::ops::Not; use crate::system::{ - Adapt, AdapterSystem, CombinatorSystem, Combine, IntoSystem, ReadOnlySystem, System, SystemIn, - SystemInput, + Adapt, AdapterSystem, CombinatorSystem, Combine, IntoSystem, ReadOnlySystem, RunSystemError, + System, SystemIn, SystemInput, }; /// A type-erased run condition stored in a [`Box`]. @@ -16,16 +17,16 @@ pub type BoxedCondition = Box>; /// /// # Marker type parameter /// -/// `Condition` trait has `Marker` type parameter, which has no special meaning, +/// `SystemCondition` trait has `Marker` type parameter, which has no special meaning, /// but exists to work around the limitation of Rust's trait system. /// /// Type parameter in return type can be set to `<()>` by calling [`IntoSystem::into_system`], /// but usually have to be specified when passing a condition to a function. /// /// ``` -/// # use bevy_ecs::schedule::Condition; +/// # use bevy_ecs::schedule::SystemCondition; /// # use bevy_ecs::system::IntoSystem; -/// fn not_condition(a: impl Condition) -> impl Condition<()> { +/// fn not_condition(a: impl SystemCondition) -> impl SystemCondition<()> { /// IntoSystem::into_system(a.map(|x| !x)) /// } /// ``` @@ -34,7 +35,7 @@ pub type BoxedCondition = Box>; /// A condition that returns true every other time it's called. /// ``` /// # use bevy_ecs::prelude::*; -/// fn every_other_time() -> impl Condition<()> { +/// fn every_other_time() -> impl SystemCondition<()> { /// IntoSystem::into_system(|mut flag: Local| { /// *flag = !*flag; /// *flag @@ -58,8 +59,8 @@ pub type BoxedCondition = Box>; /// /// ``` /// # use bevy_ecs::prelude::*; -/// fn identity() -> impl Condition<(), In> { -/// IntoSystem::into_system(|In(x)| x) +/// fn identity() -> impl SystemCondition<(), In> { +/// IntoSystem::into_system(|In(x): In| x) /// } /// /// # fn always_true() -> bool { true } @@ -71,7 +72,9 @@ pub type BoxedCondition = Box>; /// # world.insert_resource(DidRun(false)); /// # app.run(&mut world); /// # assert!(world.resource::().0); -pub trait Condition: sealed::Condition { +pub trait SystemCondition: + sealed::SystemCondition +{ /// Returns a new run condition that only returns `true` /// if both this one and the passed `and` return `true`. /// @@ -116,11 +119,11 @@ pub trait Condition: sealed::Condition /// Note that in this case, it's better to just use the run condition [`resource_exists_and_equals`]. /// /// [`resource_exists_and_equals`]: common_conditions::resource_exists_and_equals - fn and>(self, and: C) -> And { + fn and>(self, and: C) -> And { 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` @@ -168,11 +171,11 @@ pub trait Condition: sealed::Condition /// ), /// ); /// ``` - fn nand>(self, nand: C) -> Nand { + fn nand>(self, nand: C) -> Nand { 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` @@ -220,11 +223,11 @@ pub trait Condition: sealed::Condition /// ), /// ); /// ``` - fn nor>(self, nor: C) -> Nor { + fn nor>(self, nor: C) -> Nor { 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` @@ -267,11 +270,11 @@ pub trait Condition: sealed::Condition /// # app.run(&mut world); /// # assert!(world.resource::().0); /// ``` - fn or>(self, or: C) -> Or { + fn or>(self, or: C) -> Or { 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` @@ -319,11 +322,11 @@ pub trait Condition: sealed::Condition /// ), /// ); /// ``` - fn xnor>(self, xnor: C) -> Xnor { + fn xnor>(self, xnor: C) -> Xnor { 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` @@ -361,20 +364,23 @@ pub trait Condition: sealed::Condition /// ); /// # app.run(&mut world); /// ``` - fn xor>(self, xor: C) -> Xor { + fn xor>(self, xor: C) -> Xor { 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)) } } -impl Condition for F where F: sealed::Condition {} +impl SystemCondition for F where + F: sealed::SystemCondition +{ +} mod sealed { use crate::system::{IntoSystem, ReadOnlySystem, SystemInput}; - pub trait Condition: + pub trait SystemCondition: IntoSystem { // This associated type is necessary to let the compiler @@ -382,7 +388,7 @@ mod sealed { type ReadOnlySystem: ReadOnlySystem; } - impl Condition for F + impl SystemCondition for F where F: IntoSystem, F::System: ReadOnlySystem, @@ -391,21 +397,21 @@ mod sealed { } } -/// A collection of [run conditions](Condition) that may be useful in any bevy app. +/// A collection of [run conditions](SystemCondition) that may be useful in any bevy app. pub mod common_conditions { - use super::{Condition, NotSystem}; + use super::{NotSystem, SystemCondition}; use crate::{ change_detection::DetectChanges, - event::{Event, EventReader}, + event::{BufferedEvent, EventReader}, + lifecycle::RemovedComponents, prelude::{Component, Query, With}, query::QueryFilter, - removal_detection::RemovedComponents, resource::Resource, system::{In, IntoSystem, Local, Res, System, SystemInput}, }; use alloc::format; - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// on the first time the condition is run and false every time after. /// /// # Example @@ -443,7 +449,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if the resource exists. /// /// # Example @@ -478,7 +484,7 @@ pub mod common_conditions { res.is_some() } - /// Generates a [`Condition`]-satisfying closure that returns `true` + /// Generates a [`SystemCondition`]-satisfying closure that returns `true` /// if the resource is equal to `value`. /// /// # Panics @@ -518,7 +524,7 @@ pub mod common_conditions { move |res: Res| *res == value } - /// Generates a [`Condition`]-satisfying closure that returns `true` + /// Generates a [`SystemCondition`]-satisfying closure that returns `true` /// if the resource exists and is equal to `value`. /// /// The condition will return `false` if the resource does not exist. @@ -563,7 +569,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if the resource of the given type has been added since the condition was last checked. /// /// # Example @@ -604,13 +610,12 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` - /// if the resource of the given type has had its value changed since the condition - /// was last checked. + /// A [`SystemCondition`]-satisfying system that returns `true` + /// if the resource of the given type has been added or mutably dereferenced + /// since the condition was last checked. /// - /// The value is considered changed when it is added. The first time this condition - /// is checked after the resource was added, it will return `true`. - /// Change detection behaves like this everywhere in Bevy. + /// **Note** that simply *mutably dereferencing* a resource is considered a change ([`DerefMut`](std::ops::DerefMut)). + /// Bevy does not compare resources to their previous values. /// /// # Panics /// @@ -658,15 +663,12 @@ pub mod common_conditions { res.is_changed() } - /// A [`Condition`]-satisfying system that returns `true` - /// if the resource of the given type has had its value changed since the condition + /// A [`SystemCondition`]-satisfying system that returns `true` + /// if the resource of the given type has been added or mutably dereferenced since the condition /// was last checked. /// - /// The value is considered changed when it is added. The first time this condition - /// is checked after the resource was added, it will return `true`. - /// Change detection behaves like this everywhere in Bevy. - /// - /// This run condition does not detect when the resource is removed. + /// **Note** that simply *mutably dereferencing* a resource is considered a change ([`DerefMut`](std::ops::DerefMut)). + /// Bevy does not compare resources to their previous values. /// /// The condition will return `false` if the resource does not exist. /// @@ -718,16 +720,12 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` - /// if the resource of the given type has had its value changed since the condition + /// A [`SystemCondition`]-satisfying system that returns `true` + /// if the resource of the given type has been added, removed or mutably dereferenced since the condition /// was last checked. /// - /// The value is considered changed when it is added. The first time this condition - /// is checked after the resource was added, it will return `true`. - /// Change detection behaves like this everywhere in Bevy. - /// - /// This run condition also detects removal. It will return `true` if the resource - /// has been removed since the run condition was last checked. + /// **Note** that simply *mutably dereferencing* a resource is considered a change ([`DerefMut`](std::ops::DerefMut)). + /// Bevy does not compare resources to their previous values. /// /// The condition will return `false` if the resource does not exist. /// @@ -795,7 +793,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if the resource of the given type has been removed since the condition was last checked. /// /// # Example @@ -847,7 +845,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any new events of the given type since it was last called. /// /// # Example @@ -866,7 +864,7 @@ pub mod common_conditions { /// my_system.run_if(on_event::), /// ); /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent; /// /// fn my_system(mut counter: ResMut) { @@ -877,13 +875,13 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 0); /// - /// world.resource_mut::>().send(MyEvent); + /// world.resource_mut::>().write(MyEvent); /// /// // A `MyEvent` event has been pushed so `my_system` will run /// 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), @@ -891,7 +889,7 @@ pub mod common_conditions { reader.read().count() > 0 } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any entities with the given component type. /// /// # Example @@ -928,7 +926,7 @@ pub mod common_conditions { !query.is_empty() } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any entity with a component of the given type removed. pub fn any_component_removed(mut removals: RemovedComponents) -> bool { // `RemovedComponents` based on events and therefore events need to be consumed, @@ -939,13 +937,13 @@ pub mod common_conditions { removals.read().count() > 0 } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any entities that match the given [`QueryFilter`]. pub fn any_match_filter(query: Query<(), F>) -> bool { !query.is_empty() } - /// Generates a [`Condition`] that inverses the result of passed one. + /// Generates a [`SystemCondition`] that inverses the result of passed one. /// /// # Example /// @@ -984,7 +982,7 @@ pub mod common_conditions { NotSystem::new(super::NotMarker, condition, name.into()) } - /// Generates a [`Condition`] that returns true when the passed one changes. + /// Generates a [`SystemCondition`] that returns true when the passed one changes. /// /// The first time this is called, the passed condition is assumed to have been previously false. /// @@ -1022,10 +1020,10 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 2); /// ``` - pub fn condition_changed(condition: C) -> impl Condition<(), CIn> + pub fn condition_changed(condition: C) -> impl SystemCondition<(), CIn> where CIn: SystemInput, - C: Condition, + C: SystemCondition, { IntoSystem::into_system(condition.pipe(|In(new): In, mut prev: Local| { let changed = *prev != new; @@ -1034,7 +1032,7 @@ pub mod common_conditions { })) } - /// Generates a [`Condition`] that returns true when the result of + /// Generates a [`SystemCondition`] that returns true when the result of /// the passed one went from false to true since the last time this was called. /// /// The first time this is called, the passed condition is assumed to have been previously false. @@ -1078,10 +1076,13 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 2); /// ``` - pub fn condition_changed_to(to: bool, condition: C) -> impl Condition<(), CIn> + pub fn condition_changed_to( + to: bool, + condition: C, + ) -> impl SystemCondition<(), CIn> where CIn: SystemInput, - C: Condition, + C: SystemCondition, { IntoSystem::into_system(condition.pipe( move |In(new): In, mut prev: Local| -> bool { @@ -1110,9 +1111,9 @@ impl> Adapt for NotMarker { fn adapt( &mut self, input: ::Inner<'_>, - run_system: impl FnOnce(SystemIn<'_, S>) -> S::Out, - ) -> Self::Out { - !run_system(input) + run_system: impl FnOnce(SystemIn<'_, S>) -> Result, + ) -> Result { + run_system(input).map(Not::not) } } @@ -1148,10 +1149,10 @@ where fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> A::Out, - b: impl FnOnce(SystemIn<'_, A>) -> B::Out, - ) -> Self::Out { - a(input) && b(input) + a: impl FnOnce(SystemIn<'_, A>) -> Result, + b: impl FnOnce(SystemIn<'_, A>) -> Result, + ) -> Result { + Ok(a(input)? && b(input)?) } } @@ -1169,10 +1170,10 @@ where fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> A::Out, - b: impl FnOnce(SystemIn<'_, B>) -> B::Out, - ) -> Self::Out { - !(a(input) && b(input)) + a: impl FnOnce(SystemIn<'_, A>) -> Result, + b: impl FnOnce(SystemIn<'_, A>) -> Result, + ) -> Result { + Ok(!(a(input)? && b(input)?)) } } @@ -1190,10 +1191,10 @@ where fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> A::Out, - b: impl FnOnce(SystemIn<'_, B>) -> B::Out, - ) -> Self::Out { - !(a(input) || b(input)) + a: impl FnOnce(SystemIn<'_, A>) -> Result, + b: impl FnOnce(SystemIn<'_, A>) -> Result, + ) -> Result { + Ok(!(a(input)? || b(input)?)) } } @@ -1211,10 +1212,10 @@ where fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> A::Out, - b: impl FnOnce(SystemIn<'_, B>) -> B::Out, - ) -> Self::Out { - a(input) || b(input) + a: impl FnOnce(SystemIn<'_, A>) -> Result, + b: impl FnOnce(SystemIn<'_, A>) -> Result, + ) -> Result { + Ok(a(input)? || b(input)?) } } @@ -1232,10 +1233,10 @@ where fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> A::Out, - b: impl FnOnce(SystemIn<'_, B>) -> B::Out, - ) -> Self::Out { - !(a(input) ^ b(input)) + a: impl FnOnce(SystemIn<'_, A>) -> Result, + b: impl FnOnce(SystemIn<'_, A>) -> Result, + ) -> Result { + Ok(!(a(input)? ^ b(input)?)) } } @@ -1253,16 +1254,17 @@ where fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> A::Out, - b: impl FnOnce(SystemIn<'_, B>) -> B::Out, - ) -> Self::Out { - a(input) ^ b(input) + a: impl FnOnce(SystemIn<'_, A>) -> Result, + b: impl FnOnce(SystemIn<'_, A>) -> Result, + ) -> Result { + Ok(a(input)? ^ b(input)?) } } #[cfg(test)] mod tests { - use super::{common_conditions::*, Condition}; + use super::{common_conditions::*, SystemCondition}; + use crate::event::{BufferedEvent, Event}; use crate::query::With; use crate::{ change_detection::ResMut, @@ -1271,7 +1273,7 @@ mod tests { system::Local, world::World, }; - use bevy_ecs_macros::{Event, Resource}; + use bevy_ecs_macros::Resource; #[derive(Resource, Default)] struct Counter(usize); @@ -1382,7 +1384,7 @@ mod tests { #[derive(Component)] struct TestComponent; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent; #[derive(Resource)] diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index b98205e32b..a8dbfc810b 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -2,23 +2,21 @@ use alloc::{boxed::Box, vec, vec::Vec}; use variadics_please::all_tuples; use crate::{ - error::Result, - never::Never, schedule::{ auto_insert_apply_deferred::IgnoreDeferred, - condition::{BoxedCondition, Condition}, + condition::{BoxedCondition, SystemCondition}, graph::{Ambiguity, Dependency, DependencyKind, GraphInfo}, set::{InternedSystemSet, IntoSystemSet, SystemSet}, Chain, }, - system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System}, + system::{BoxedSystem, IntoSystem, ScheduleSystem, System}, }; -fn new_condition(condition: impl Condition) -> BoxedCondition { +fn new_condition(condition: impl SystemCondition) -> BoxedCondition { let condition_system = IntoSystem::into_system(condition); assert!( condition_system.is_send(), - "Condition `{}` accesses `NonSend` resources. This is not currently supported.", + "SystemCondition `{}` accesses `NonSend` resources. This is not currently supported.", condition_system.name() ); @@ -191,7 +189,7 @@ impl> ScheduleConfig } } - fn distributive_run_if_inner(&mut self, condition: impl Condition + Clone) { + fn distributive_run_if_inner(&mut self, condition: impl SystemCondition + Clone) { match self { Self::ScheduleConfig(config) => { config.conditions.push(new_condition(condition)); @@ -382,8 +380,8 @@ pub trait IntoScheduleConfigs(self, condition: impl Condition + Clone) -> ScheduleConfigs { + fn distributive_run_if( + self, + condition: impl SystemCondition + Clone, + ) -> ScheduleConfigs { self.into_configs().distributive_run_if(condition) } - /// Run the systems only if the [`Condition`] is `true`. + /// Run the systems only if the [`SystemCondition`] is `true`. /// - /// The `Condition` will be evaluated at most once (per schedule run), + /// The `SystemCondition` will be evaluated at most once (per schedule run), /// the first time a system in this set prepares to run. /// /// If this set contains more than one system, calling `run_if` is equivalent to adding each @@ -444,7 +445,7 @@ pub trait IntoScheduleConfigs(self, condition: impl Condition) -> ScheduleConfigs { + fn run_if(self, condition: impl SystemCondition) -> ScheduleConfigs { self.into_configs().run_if(condition) } @@ -526,13 +527,13 @@ impl> IntoScheduleCo fn distributive_run_if( mut self, - condition: impl Condition + Clone, + condition: impl SystemCondition + Clone, ) -> ScheduleConfigs { self.distributive_run_if_inner(condition); self } - fn run_if(mut self, condition: impl Condition) -> ScheduleConfigs { + fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { self.run_if_dyn(new_condition(condition)); self } @@ -557,37 +558,9 @@ impl> IntoScheduleCo } } -/// Marker component to allow for conflicting implementations of [`IntoScheduleConfigs`] -#[doc(hidden)] -pub struct Infallible; - -impl IntoScheduleConfigs for F +impl IntoScheduleConfigs for F where F: IntoSystem<(), (), Marker>, -{ - fn into_configs(self) -> ScheduleConfigs { - let wrapper = InfallibleSystemWrapper::new(IntoSystem::into_system(self)); - ScheduleConfigs::ScheduleConfig(ScheduleSystem::into_config(Box::new(wrapper))) - } -} - -impl IntoScheduleConfigs for F -where - F: IntoSystem<(), Never, Marker>, -{ - fn into_configs(self) -> ScheduleConfigs { - let wrapper = InfallibleSystemWrapper::new(IntoSystem::into_system(self)); - ScheduleConfigs::ScheduleConfig(ScheduleSystem::into_config(Box::new(wrapper))) - } -} - -/// Marker component to allow for conflicting implementations of [`IntoScheduleConfigs`] -#[doc(hidden)] -pub struct Fallible; - -impl IntoScheduleConfigs for F -where - F: IntoSystem<(), Result, Marker>, { fn into_configs(self) -> ScheduleConfigs { let boxed_system = Box::new(IntoSystem::into_system(self)); @@ -595,7 +568,7 @@ where } } -impl IntoScheduleConfigs for BoxedSystem<(), Result> { +impl IntoScheduleConfigs for BoxedSystem<(), ()> { fn into_configs(self) -> ScheduleConfigs { ScheduleConfigs::ScheduleConfig(ScheduleSystem::into_config(self)) } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index a601284fb0..12030c2580 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,13 +16,18 @@ pub use self::multi_threaded::{MainThreadExecutor, MultiThreadedExecutor}; use fixedbitset::FixedBitSet; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, - query::{Access, FilteredAccessSet}, - schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, - system::{ScheduleSystem, System, SystemIn, SystemParamValidationError}, + query::FilteredAccessSet, + schedule::{ + ConditionWithAccess, InternedSystemSet, SystemKey, SystemSetKey, SystemTypeSet, + SystemWithAccess, + }, + system::{ + RunSystemError, ScheduleSystem, System, SystemIn, SystemParamValidationError, + SystemStateFlags, + }, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; @@ -73,11 +79,11 @@ pub enum ExecutorKind { #[derive(Default)] pub struct SystemSchedule { /// List of system node ids. - pub(super) system_ids: Vec, + 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( @@ -96,9 +102,9 @@ pub struct SystemSchedule { /// List of sets containing the system that have conditions pub(super) sets_with_conditions_of_systems: Vec, /// List of system set node ids. - pub(super) set_ids: Vec, + 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. /// @@ -157,59 +163,36 @@ pub(super) fn is_apply_deferred(system: &ScheduleSystem) -> bool { impl System for ApplyDeferred { type In = (); - type Out = Result<()>; + type Out = (); - fn name(&self) -> Cow<'static, str> { - Cow::Borrowed("bevy_ecs::apply_deferred") + fn name(&self) -> DebugName { + DebugName::borrowed("bevy_ecs::apply_deferred") } - fn component_access(&self) -> &Access { - // This system accesses no components. - const { &Access::new() } - } - - fn component_access_set(&self) -> &FilteredAccessSet { - const { &FilteredAccessSet::new() } - } - - fn archetype_component_access(&self) -> &Access { - // This system accesses no archetype components. - const { &Access::new() } - } - - fn is_send(&self) -> bool { - // Although this system itself does nothing on its own, the system - // executor uses it to apply deferred commands. Commands must be allowed - // to access non-send resources, so this system must be non-send for - // scheduling purposes. - false - } - - fn is_exclusive(&self) -> bool { - // This system is labeled exclusive because it is used by the system - // executor to find places where deferred commands should be applied, - // and commands can only be applied with exclusive access to the world. - true - } - - fn has_deferred(&self) -> bool { - // This system itself doesn't have any commands to apply, but when it - // is pulled from the schedule to be ran, the executor will apply - // deferred commands from other systems. - false + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } unsafe fn run_unsafe( &mut self, _input: SystemIn<'_, Self>, _world: UnsafeWorldCell, - ) -> Self::Out { + ) -> Result { // This system does nothing on its own. The executor will apply deferred // commands from other systems instead of running this system. Ok(()) } - fn run(&mut self, _input: SystemIn<'_, Self>, _world: &mut World) -> Self::Out { + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) {} + + fn run( + &mut self, + _input: SystemIn<'_, Self>, + _world: &mut World, + ) -> Result { // This system does nothing on its own. The executor will apply deferred // commands from other systems instead of running this system. Ok(()) @@ -228,11 +211,11 @@ impl System for ApplyDeferred { Ok(()) } - fn initialize(&mut self, _world: &mut World) {} + fn initialize(&mut self, _world: &mut World) -> FilteredAccessSet { + FilteredAccessSet::new() + } - fn update_archetype_component_access(&mut self, _world: UnsafeWorldCell) {} - - 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()] @@ -269,7 +252,7 @@ mod __rust_begin_short_backtrace { use crate::world::unsafe_world_cell::UnsafeWorldCell; use crate::{ error::Result, - system::{ReadOnlySystem, ScheduleSystem}, + system::{ReadOnlySystem, RunSystemError, ScheduleSystem}, world::World, }; @@ -278,7 +261,10 @@ mod __rust_begin_short_backtrace { // This is only used by `MultiThreadedExecutor`, and would be dead code without `std`. #[cfg(feature = "std")] #[inline(never)] - pub(super) unsafe fn run_unsafe(system: &mut ScheduleSystem, world: UnsafeWorldCell) -> Result { + pub(super) unsafe fn run_unsafe( + system: &mut ScheduleSystem, + world: UnsafeWorldCell, + ) -> Result<(), RunSystemError> { let result = system.run_unsafe((), world); // Call `black_box` to prevent this frame from being tail-call optimized away black_box(()); @@ -293,13 +279,16 @@ mod __rust_begin_short_backtrace { pub(super) unsafe fn readonly_run_unsafe( system: &mut dyn ReadOnlySystem, world: UnsafeWorldCell, - ) -> O { + ) -> Result { // Call `black_box` to prevent this frame from being tail-call optimized away black_box(system.run_unsafe((), world)) } #[inline(never)] - pub(super) fn run(system: &mut ScheduleSystem, world: &mut World) -> Result { + pub(super) fn run( + system: &mut ScheduleSystem, + world: &mut World, + ) -> Result<(), RunSystemError> { let result = system.run((), world); // Call `black_box` to prevent this frame from being tail-call optimized away black_box(()); @@ -310,7 +299,7 @@ mod __rust_begin_short_backtrace { pub(super) fn run_without_applying_deferred( system: &mut ScheduleSystem, world: &mut World, - ) -> Result { + ) -> Result<(), RunSystemError> { let result = system.run_without_applying_deferred((), world); // Call `black_box` to prevent this frame from being tail-call optimized away black_box(()); @@ -321,7 +310,7 @@ mod __rust_begin_short_backtrace { pub(super) fn readonly_run( system: &mut dyn ReadOnlySystem, world: &mut World, - ) -> O { + ) -> Result { // Call `black_box` to prevent this frame from being tail-call optimized away black_box(system.run((), world)) } @@ -370,7 +359,7 @@ mod tests { #[expect(clippy::print_stdout, reason = "std and println are allowed in tests")] fn single_and_populated_skipped_and_run() { for executor in EXECUTORS { - std::println!("Testing executor: {:?}", executor); + std::println!("Testing executor: {executor:?}"); let mut world = World::new(); world.init_resource::(); @@ -453,13 +442,16 @@ mod tests { #[test] fn piped_system_second_system_skipped() { + // This system will be run before the second system is validated fn pipe_out(mut counter: ResMut) -> u8 { counter.0 += 1; 42 } // This system should be skipped when run due to no matching entity - fn pipe_in(_input: In, _single: Single<&TestComponent>) {} + fn pipe_in(_input: In, _single: Single<&TestComponent>, mut counter: ResMut) { + counter.0 += 1; + } let mut world = World::new(); world.init_resource::(); @@ -468,7 +460,7 @@ mod tests { schedule.add_systems(pipe_out.pipe(pipe_in)); schedule.run(&mut world); let counter = world.resource::(); - assert_eq!(counter.0, 0); + assert_eq!(counter.0, 1); } #[test] diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index b0757cc031..006faa8fe4 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -1,7 +1,7 @@ use alloc::{boxed::Box, vec::Vec}; +use bevy_platform::cell::SyncUnsafeCell; use bevy_platform::sync::Arc; use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor}; -use bevy_utils::syncunsafecell::SyncUnsafeCell; use concurrent_queue::ConcurrentQueue; use core::{any::Any, panic::AssertUnwindSafe}; use fixedbitset::FixedBitSet; @@ -13,26 +13,31 @@ use std::sync::{Mutex, MutexGuard}; use tracing::{info_span, Span}; use crate::{ - error::{default_error_handler, BevyError, ErrorContext, Result}, + error::{ErrorContext, ErrorHandler, Result}, prelude::Resource, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, - system::ScheduleSystem, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + SystemWithAccess, + }, + system::{RunSystemError, ScheduleSystem}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; 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], } @@ -134,7 +139,7 @@ pub struct ExecutorState { struct Context<'scope, 'env, 'sys> { environment: &'env Environment<'env, 'sys>, scope: &'scope Scope<'scope, 'env, ()>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, } impl Default for MultiThreadedExecutor { @@ -170,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); @@ -185,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); @@ -200,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); @@ -218,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); } } @@ -240,7 +240,7 @@ impl SystemExecutor for MultiThreadedExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { let state = self.state.get_mut().unwrap(); // reset counts @@ -342,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 { @@ -353,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 @@ -443,6 +447,14 @@ impl ExecutorState { return; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !context + .environment + .world_cell + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + // can't borrow since loop mutably borrows `self` let mut ready_systems = core::mem::take(&mut self.ready_systems_copy); @@ -458,14 +470,15 @@ 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; - if !self.can_run( - system_index, - system, - conditions, - context.environment.world_cell, - ) { + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + + if !self.can_run(system_index, conditions) { // NOTE: exclusive systems with ambiguities are susceptible to // being significantly displaced here (compared to single-threaded order) // if systems after them in topological order can run @@ -476,7 +489,6 @@ impl ExecutorState { self.ready_systems.remove(system_index); // SAFETY: `can_run` returned true, which means that: - // - It must have called `update_archetype_component_access` for each run condition. // - There can be no systems running whose accesses would conflict with any conditions. if unsafe { !self.should_run( @@ -484,6 +496,7 @@ impl ExecutorState { system, conditions, context.environment.world_cell, + context.error_handler, ) } { self.skip_system_and_signal_dependents(system_index); @@ -509,7 +522,6 @@ impl ExecutorState { // - Caller ensured no other reference to this system exists. // - `system_task_metadata[system_index].is_exclusive` is `false`, // so `System::is_exclusive` returned `false` when we called it. - // - `can_run` has been called, which calls `update_archetype_component_access` with this system. // - `can_run` returned true, so no systems with conflicting world access are running. unsafe { self.spawn_system_task(context, system_index); @@ -521,13 +533,7 @@ impl ExecutorState { self.ready_systems_copy = ready_systems; } - fn can_run( - &mut self, - system_index: usize, - system: &mut ScheduleSystem, - conditions: &mut Conditions, - world: UnsafeWorldCell, - ) -> bool { + fn can_run(&mut self, system_index: usize, conditions: &mut Conditions) -> bool { let system_meta = &self.system_task_metadata[system_index]; if system_meta.is_exclusive && self.num_running_systems > 0 { return false; @@ -541,17 +547,11 @@ impl ExecutorState { for set_idx in conditions.sets_with_conditions_of_systems[system_index] .difference(&self.evaluated_sets) { - for condition in &mut conditions.set_conditions[set_idx] { - condition.update_archetype_component_access(world); - } if !self.set_condition_conflicting_systems[set_idx].is_disjoint(&self.running_systems) { return false; } } - for condition in &mut conditions.system_conditions[system_index] { - condition.update_archetype_component_access(world); - } if !system_meta .condition_conflicting_systems .is_disjoint(&self.running_systems) @@ -559,14 +559,12 @@ impl ExecutorState { return false; } - if !self.skipped_systems.contains(system_index) { - system.update_archetype_component_access(world); - if !system_meta + if !self.skipped_systems.contains(system_index) + && !system_meta .conflicting_systems .is_disjoint(&self.running_systems) - { - return false; - } + { + return false; } true @@ -576,17 +574,15 @@ impl ExecutorState { /// * `world` must have permission to read any world data required by /// the system's conditions: this includes conditions for the system /// itself, and conditions for any of the system's sets. - /// * `update_archetype_component` must have been called with `world` - /// for the system as well as system and system set's run conditions. unsafe fn should_run( &mut self, system_index: usize, system: &mut ScheduleSystem, conditions: &mut Conditions, world: UnsafeWorldCell, + error_handler: ErrorHandler, ) -> bool { let mut should_run = !self.skipped_systems.contains(system_index); - let error_handler = default_error_handler(); for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() { if self.evaluated_sets.contains(set_idx) { @@ -597,9 +593,12 @@ impl ExecutorState { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the conditions. - // - `update_archetype_component_access` has been called for each run condition. let set_conditions_met = unsafe { - evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world) + evaluate_and_fold_conditions( + &mut conditions.set_conditions[set_idx], + world, + error_handler, + ) }; if !set_conditions_met { @@ -615,9 +614,12 @@ impl ExecutorState { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the conditions. - // - `update_archetype_component_access` has been called for each run condition. let system_conditions_met = unsafe { - evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world) + evaluate_and_fold_conditions( + &mut conditions.system_conditions[system_index], + world, + error_handler, + ) }; if !system_conditions_met { @@ -630,7 +632,6 @@ impl ExecutorState { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the system. - // - `update_archetype_component_access` has been called for system. let valid_params = match unsafe { system.validate_param_unsafe(world) } { Ok(()) => true, Err(e) => { @@ -661,11 +662,9 @@ impl ExecutorState { /// - `is_exclusive` must have returned `false` for the specified system. /// - `world` must have permission to access the world data /// used by the specified system. - /// - `update_archetype_component_access` must have been called with `world` - /// on the system associated with `system_index`. 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; @@ -677,12 +676,13 @@ impl ExecutorState { // - The caller ensures that we have permission to // access the world data used by the system. // - `is_exclusive` returned false - // - `update_archetype_component_access` has been called. unsafe { - if let Err(err) = __rust_begin_short_backtrace::run_unsafe( - system, - context.environment.world_cell, - ) { + if let Err(RunSystemError::Failed(err)) = + __rust_begin_short_backtrace::run_unsafe( + system, + context.environment.world_cell, + ) + { (context.error_handler)( err, ErrorContext::System { @@ -708,7 +708,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; @@ -731,7 +731,9 @@ impl ExecutorState { // that no other systems currently have access to the world. let world = unsafe { context.environment.world_cell.world_mut() }; let res = std::panic::catch_unwind(AssertUnwindSafe(|| { - if let Err(err) = __rust_begin_short_backtrace::run(system, world) { + if let Err(RunSystemError::Failed(err)) = + __rust_begin_short_backtrace::run(system, world) + { (context.error_handler)( err, ErrorContext::System { @@ -790,12 +792,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); })); @@ -805,7 +807,7 @@ fn apply_deferred( { eprintln!( "Encountered a panic when applying buffers for system `{}`!", - &*system.name() + system.name() ); } return Err(payload); @@ -817,45 +819,44 @@ fn apply_deferred( /// # Safety /// - `world` must have permission to read any world data /// required by `conditions`. -/// - `update_archetype_component_access` must have been called -/// with `world` for each condition in `conditions`. unsafe fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: UnsafeWorldCell, + error_handler: ErrorHandler, ) -> bool { - let error_handler = default_error_handler(); - #[expect( clippy::unnecessary_fold, reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the condition. - // - `update_archetype_component_access` has been called for condition. - match unsafe { condition.validate_param_unsafe(world) } { - Ok(()) => (), - Err(e) => { - if !e.skipped { + unsafe { condition.validate_param_unsafe(world) } + .map_err(From::from) + .and_then(|()| { + // SAFETY: + // - The caller ensures that `world` has permission to read any data + // required by the condition. + // - `update_archetype_component_access` has been called for condition. + unsafe { + __rust_begin_short_backtrace::readonly_run_unsafe(&mut **condition, world) + } + }) + .unwrap_or_else(|err| { + if let RunSystemError::Failed(err) = err { error_handler( - e.into(), + err, ErrorContext::System { name: condition.name(), last_run: condition.get_last_run(), }, ); - } - return false; - } - } - // SAFETY: - // - The caller ensures that `world` has permission to read any data - // required by the condition. - // - `update_archetype_component_access` has been called for condition. - unsafe { __rust_begin_short_backtrace::readonly_run_unsafe(&mut **condition, world) } + }; + false + }) }) .fold(true, |acc, res| acc && res) } diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 701a8d8f06..17f3f3b8a0 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -10,12 +10,16 @@ use tracing::info_span; use std::eprintln; use crate::{ - error::{default_error_handler, BevyError, ErrorContext}, + error::{ErrorContext, ErrorHandler}, schedule::{ - executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, + executor::is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, + SystemSchedule, }, + system::RunSystemError, world::World, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -50,7 +54,7 @@ impl SystemExecutor for SimpleExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { // If stepping is enabled, make sure we skip those systems that should // not be run. @@ -60,11 +64,17 @@ impl SystemExecutor for SimpleExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + 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() { @@ -73,8 +83,11 @@ impl SystemExecutor for SimpleExecutor { } // evaluate system set's conditions - let set_conditions_met = - evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + let set_conditions_met = evaluate_and_fold_conditions( + &mut schedule.set_conditions[set_idx], + world, + error_handler, + ); if !set_conditions_met { self.completed_systems @@ -86,34 +99,24 @@ impl SystemExecutor for SimpleExecutor { } // evaluate system's conditions - let system_conditions_met = - evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + let system_conditions_met = evaluate_and_fold_conditions( + &mut schedule.system_conditions[system_index], + world, + error_handler, + ); should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; - if should_run { - let valid_params = match system.validate_param(world) { - Ok(()) => true, - Err(e) => { - if !e.skipped { - error_handler( - e.into(), - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - } - false - } - }; - should_run &= valid_params; - } + let system = &mut schedule.systems[system_index].system; #[cfg(feature = "trace")] should_run_span.exit(); + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + // system has either been skipped or will run self.completed_systems.insert(system_index); @@ -126,7 +129,9 @@ impl SystemExecutor for SimpleExecutor { } let f = AssertUnwindSafe(|| { - if let Err(err) = __rust_begin_short_backtrace::run(system, world) { + if let Err(RunSystemError::Failed(err)) = + __rust_begin_short_backtrace::run(system, world) + { error_handler( err, ErrorContext::System { @@ -141,7 +146,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); } } @@ -175,8 +180,16 @@ impl SimpleExecutor { since = "0.17.0", note = "Use SingleThreadedExecutor instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation." )] -fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - let error_handler = default_error_handler(); +fn evaluate_and_fold_conditions( + conditions: &mut [ConditionWithAccess], + world: &mut World, + error_handler: ErrorHandler, +) -> bool { + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); #[expect( clippy::unnecessary_fold, @@ -184,23 +197,25 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W )] conditions .iter_mut() - .map(|condition| { - match condition.validate_param(world) { - Ok(()) => (), - Err(e) => { - if !e.skipped { + .map(|ConditionWithAccess { condition, .. }| { + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + condition.refresh_hotpatch(); + } + __rust_begin_short_backtrace::readonly_run(&mut **condition, world).unwrap_or_else( + |err| { + if let RunSystemError::Failed(err) = err { error_handler( - e.into(), + err, ErrorContext::System { name: condition.name(), last_run: condition.get_last_run(), }, ); - } - return false; - } - } - __rust_begin_short_backtrace::readonly_run(&mut **condition, world) + }; + false + }, + ) }) .fold(true, |acc, res| acc && res) } diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 82e9e354a8..4d321bdaff 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -8,10 +8,15 @@ use tracing::info_span; use std::eprintln; use crate::{ - error::{default_error_handler, BevyError, ErrorContext}, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + error::{ErrorContext, ErrorHandler}, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + }, + system::RunSystemError, world::World, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -50,7 +55,7 @@ impl SystemExecutor for SingleThreadedExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { // If stepping is enabled, make sure we skip those systems that should // not be run. @@ -60,11 +65,17 @@ impl SystemExecutor for SingleThreadedExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + 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() { @@ -73,8 +84,11 @@ impl SystemExecutor for SingleThreadedExecutor { } // evaluate system set's conditions - let set_conditions_met = - evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + let set_conditions_met = evaluate_and_fold_conditions( + &mut schedule.set_conditions[set_idx], + world, + error_handler, + ); if !set_conditions_met { self.completed_systems @@ -86,35 +100,24 @@ impl SystemExecutor for SingleThreadedExecutor { } // evaluate system's conditions - let system_conditions_met = - evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + let system_conditions_met = evaluate_and_fold_conditions( + &mut schedule.system_conditions[system_index], + world, + error_handler, + ); should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; - if should_run { - let valid_params = match system.validate_param(world) { - Ok(()) => true, - Err(e) => { - if !e.skipped { - error_handler( - e.into(), - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - } - false - } - }; - - should_run &= valid_params; - } + let system = &mut schedule.systems[system_index].system; #[cfg(feature = "trace")] should_run_span.exit(); + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + // system has either been skipped or will run self.completed_systems.insert(system_index); @@ -128,7 +131,7 @@ impl SystemExecutor for SingleThreadedExecutor { } let f = AssertUnwindSafe(|| { - if let Err(err) = + if let Err(RunSystemError::Failed(err)) = __rust_begin_short_backtrace::run_without_applying_deferred(system, world) { error_handler( @@ -145,7 +148,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); } } @@ -185,7 +188,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); } @@ -193,8 +196,16 @@ impl SingleThreadedExecutor { } } -fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - let error_handler: fn(BevyError, ErrorContext) = default_error_handler(); +fn evaluate_and_fold_conditions( + conditions: &mut [ConditionWithAccess], + world: &mut World, + error_handler: ErrorHandler, +) -> bool { + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); #[expect( clippy::unnecessary_fold, @@ -202,23 +213,25 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W )] conditions .iter_mut() - .map(|condition| { - match condition.validate_param(world) { - Ok(()) => (), - Err(e) => { - if !e.skipped { + .map(|ConditionWithAccess { condition, .. }| { + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + condition.refresh_hotpatch(); + } + __rust_begin_short_backtrace::readonly_run(&mut **condition, world).unwrap_or_else( + |err| { + if let RunSystemError::Failed(err) = err { error_handler( - e.into(), + err, ErrorContext::System { name: condition.name(), last_run: condition.get_last_run(), }, ); - } - return false; - } - } - __rust_begin_short_backtrace::readonly_run(&mut **condition, world) + }; + false + }, + ) }) .fold(true, |acc, res| acc && res) } diff --git a/crates/bevy_ecs/src/schedule/graph/graph_map.rs b/crates/bevy_ecs/src/schedule/graph/graph_map.rs index d2fbde9995..9224efe276 100644 --- a/crates/bevy_ecs/src/schedule/graph/graph_map.rs +++ b/crates/bevy_ecs/src/schedule/graph/graph_map.rs @@ -11,6 +11,7 @@ use core::{ hash::{BuildHasher, Hash}, }; use indexmap::IndexMap; +use slotmap::{Key, KeyData}; use smallvec::SmallVec; use super::NodeId; @@ -298,7 +299,7 @@ impl Direction { /// Compact storage of a [`NodeId`] and a [`Direction`]. #[derive(Clone, Copy)] struct CompactNodeIdAndDirection { - index: usize, + key: KeyData, is_system: bool, direction: Direction, } @@ -310,27 +311,30 @@ impl fmt::Debug for CompactNodeIdAndDirection { } impl CompactNodeIdAndDirection { - const fn store(node: NodeId, direction: Direction) -> Self { - let index = node.index(); + fn store(node: NodeId, direction: Direction) -> Self { + let key = match node { + NodeId::System(key) => key.data(), + NodeId::Set(key) => key.data(), + }; let is_system = node.is_system(); Self { - index, + key, is_system, direction, } } - const fn load(self) -> (NodeId, Direction) { + fn load(self) -> (NodeId, Direction) { let Self { - index, + key, is_system, direction, } = self; let node = match is_system { - true => NodeId::System(index), - false => NodeId::Set(index), + true => NodeId::System(key.into()), + false => NodeId::Set(key.into()), }; (node, direction) @@ -340,8 +344,8 @@ impl CompactNodeIdAndDirection { /// Compact storage of a [`NodeId`] pair. #[derive(Clone, Copy, Hash, PartialEq, Eq)] struct CompactNodeIdPair { - index_a: usize, - index_b: usize, + key_a: KeyData, + key_b: KeyData, is_system_a: bool, is_system_b: bool, } @@ -353,37 +357,43 @@ impl fmt::Debug for CompactNodeIdPair { } impl CompactNodeIdPair { - const fn store(a: NodeId, b: NodeId) -> Self { - let index_a = a.index(); + fn store(a: NodeId, b: NodeId) -> Self { + let key_a = match a { + NodeId::System(index) => index.data(), + NodeId::Set(index) => index.data(), + }; let is_system_a = a.is_system(); - let index_b = b.index(); + let key_b = match b { + NodeId::System(index) => index.data(), + NodeId::Set(index) => index.data(), + }; let is_system_b = b.is_system(); Self { - index_a, - index_b, + key_a, + key_b, is_system_a, is_system_b, } } - const fn load(self) -> (NodeId, NodeId) { + fn load(self) -> (NodeId, NodeId) { let Self { - index_a, - index_b, + key_a, + key_b, is_system_a, is_system_b, } = self; let a = match is_system_a { - true => NodeId::System(index_a), - false => NodeId::Set(index_a), + true => NodeId::System(key_a.into()), + false => NodeId::Set(key_a.into()), }; let b = match is_system_b { - true => NodeId::System(index_b), - false => NodeId::Set(index_b), + true => NodeId::System(key_b.into()), + false => NodeId::Set(key_b.into()), }; (a, b) @@ -392,8 +402,11 @@ impl CompactNodeIdPair { #[cfg(test)] mod tests { + use crate::schedule::SystemKey; + use super::*; use alloc::vec; + use slotmap::SlotMap; /// The `Graph` type _must_ preserve the order that nodes are inserted in if /// no removals occur. Removals are permitted to swap the latest node into the @@ -402,37 +415,43 @@ mod tests { fn node_order_preservation() { use NodeId::System; + let mut slotmap = SlotMap::::with_key(); let mut graph = ::default(); - graph.add_node(System(1)); - graph.add_node(System(2)); - graph.add_node(System(3)); - graph.add_node(System(4)); + let sys1 = slotmap.insert(()); + let sys2 = slotmap.insert(()); + let sys3 = slotmap.insert(()); + let sys4 = slotmap.insert(()); + + graph.add_node(System(sys1)); + graph.add_node(System(sys2)); + graph.add_node(System(sys3)); + graph.add_node(System(sys4)); assert_eq!( graph.nodes().collect::>(), - vec![System(1), System(2), System(3), System(4)] + vec![System(sys1), System(sys2), System(sys3), System(sys4)] ); - graph.remove_node(System(1)); + graph.remove_node(System(sys1)); assert_eq!( graph.nodes().collect::>(), - vec![System(4), System(2), System(3)] + vec![System(sys4), System(sys2), System(sys3)] ); - graph.remove_node(System(4)); + graph.remove_node(System(sys4)); assert_eq!( graph.nodes().collect::>(), - vec![System(3), System(2)] + vec![System(sys3), System(sys2)] ); - graph.remove_node(System(2)); + graph.remove_node(System(sys2)); - assert_eq!(graph.nodes().collect::>(), vec![System(3)]); + assert_eq!(graph.nodes().collect::>(), vec![System(sys3)]); - graph.remove_node(System(3)); + graph.remove_node(System(sys3)); assert_eq!(graph.nodes().collect::>(), vec![]); } @@ -444,18 +463,26 @@ mod tests { fn strongly_connected_components() { use NodeId::System; + let mut slotmap = SlotMap::::with_key(); let mut graph = ::default(); - graph.add_edge(System(1), System(2)); - graph.add_edge(System(2), System(1)); + let sys1 = slotmap.insert(()); + let sys2 = slotmap.insert(()); + let sys3 = slotmap.insert(()); + let sys4 = slotmap.insert(()); + let sys5 = slotmap.insert(()); + let sys6 = slotmap.insert(()); - graph.add_edge(System(2), System(3)); - graph.add_edge(System(3), System(2)); + graph.add_edge(System(sys1), System(sys2)); + graph.add_edge(System(sys2), System(sys1)); - graph.add_edge(System(4), System(5)); - graph.add_edge(System(5), System(4)); + graph.add_edge(System(sys2), System(sys3)); + graph.add_edge(System(sys3), System(sys2)); - graph.add_edge(System(6), System(2)); + graph.add_edge(System(sys4), System(sys5)); + graph.add_edge(System(sys5), System(sys4)); + + graph.add_edge(System(sys6), System(sys2)); let sccs = graph .iter_sccs() @@ -465,9 +492,9 @@ mod tests { assert_eq!( sccs, vec![ - vec![System(3), System(2), System(1)], - vec![System(5), System(4)], - vec![System(6)] + vec![System(sys3), System(sys2), System(sys1)], + vec![System(sys5), System(sys4)], + vec![System(sys6)] ] ); } diff --git a/crates/bevy_ecs/src/schedule/graph/node.rs b/crates/bevy_ecs/src/schedule/graph/node.rs index e4af143fc7..40f4f53988 100644 --- a/crates/bevy_ecs/src/schedule/graph/node.rs +++ b/crates/bevy_ecs/src/schedule/graph/node.rs @@ -1,24 +1,19 @@ use core::fmt::Debug; +use crate::schedule::{SystemKey, SystemSetKey}; + /// Unique identifier for a system or system set stored in a [`ScheduleGraph`]. /// /// [`ScheduleGraph`]: crate::schedule::ScheduleGraph #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum NodeId { /// Identifier for a system. - System(usize), + System(SystemKey), /// Identifier for a system set. - Set(usize), + Set(SystemSetKey), } impl NodeId { - /// Returns the internal integer value. - pub const fn index(&self) -> usize { - match self { - NodeId::System(index) | NodeId::Set(index) => *index, - } - } - /// Returns `true` if the identified node is a system. pub const fn is_system(&self) -> bool { matches!(self, NodeId::System(_)) @@ -29,19 +24,19 @@ impl NodeId { matches!(self, NodeId::Set(_)) } - /// Compare this [`NodeId`] with another. - pub const fn cmp(&self, other: &Self) -> core::cmp::Ordering { - use core::cmp::Ordering::{Equal, Greater, Less}; - use NodeId::{Set, System}; + /// Returns the system key if the node is a system, otherwise `None`. + pub const fn as_system(&self) -> Option { + match self { + NodeId::System(system) => Some(*system), + NodeId::Set(_) => None, + } + } - match (self, other) { - (System(a), System(b)) | (Set(a), Set(b)) => match a.checked_sub(*b) { - None => Less, - Some(0) => Equal, - Some(_) => Greater, - }, - (System(_), Set(_)) => Less, - (Set(_), System(_)) => Greater, + /// Returns the system set key if the node is a system set, otherwise `None`. + pub const fn as_set(&self) -> Option { + match self { + NodeId::System(_) => None, + NodeId::Set(set) => Some(*set), } } } diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 81912d2f72..80189d58c1 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}; pub use crate::{ @@ -49,10 +51,10 @@ mod tests { struct SystemOrder(Vec); #[derive(Resource, Default)] - struct RunConditionBool(pub bool); + struct RunConditionBool(bool); #[derive(Resource, Default)] - struct Counter(pub AtomicU32); + struct Counter(AtomicU32); fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) { move |world| world.resource_mut::().0.push(tag) @@ -252,12 +254,16 @@ mod tests { } mod conditions { - use crate::change_detection::DetectChanges; + + use crate::{ + change_detection::DetectChanges, + error::{ignore, DefaultErrorHandler, Result}, + }; use super::*; #[test] - fn system_with_condition() { + fn system_with_condition_bool() { let mut world = World::default(); let mut schedule = Schedule::default(); @@ -276,6 +282,28 @@ mod tests { assert_eq!(world.resource::().0, vec![0]); } + #[test] + fn system_with_condition_result_bool() { + let mut world = World::default(); + world.insert_resource(DefaultErrorHandler(ignore)); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems(( + make_function_system(0).run_if(|| -> Result { Err(core::fmt::Error.into()) }), + make_function_system(1).run_if(|| -> Result { Ok(false) }), + )); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(2).run_if(|| -> Result { Ok(true) })); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![2]); + } + #[test] fn systems_with_distributive_condition() { let mut world = World::default(); @@ -727,6 +755,7 @@ mod tests { } mod system_ambiguity { + #[cfg(feature = "trace")] use alloc::collections::BTreeSet; use super::*; @@ -741,8 +770,7 @@ mod tests { #[derive(Component)] struct B; - // An event type - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct E; #[derive(Resource, Component)] @@ -874,7 +902,6 @@ mod tests { } #[test] - #[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"] fn filtered_components() { let mut world = World::new(); world.spawn(A); @@ -1069,6 +1096,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) {} @@ -1142,6 +1170,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..c980378dd8 100644 --- a/crates/bevy_ecs/src/schedule/pass.rs +++ b/crates/bevy_ecs/src/schedule/pass.rs @@ -2,7 +2,10 @@ use alloc::{boxed::Box, vec::Vec}; use core::any::{Any, TypeId}; use super::{DiGraph, NodeId, ScheduleBuildError, ScheduleGraph}; -use crate::world::World; +use crate::{ + schedule::{SystemKey, SystemSetKey}, + world::World, +}; use bevy_utils::TypeIdMap; use core::fmt::Debug; @@ -19,8 +22,8 @@ pub trait ScheduleBuildPass: Send + Sync + Debug + 'static { /// Instead of modifying the graph directly, this method should return an iterator of edges to add to the graph. fn collapse_set( &mut self, - set: NodeId, - systems: &[NodeId], + set: SystemSetKey, + systems: &[SystemKey], dependency_flattened: &DiGraph, ) -> impl Iterator; @@ -44,13 +47,14 @@ pub(super) trait ScheduleBuildPassObj: Send + Sync + Debug { fn collapse_set( &mut self, - set: NodeId, - systems: &[NodeId], + set: SystemSetKey, + systems: &[SystemKey], dependency_flattened: &DiGraph, dependencies_to_add: &mut Vec<(NodeId, NodeId)>, ); fn add_dependency(&mut self, from: NodeId, to: NodeId, all_options: &TypeIdMap>); } + impl ScheduleBuildPassObj for T { fn build( &mut self, @@ -62,8 +66,8 @@ impl ScheduleBuildPassObj for T { } fn collapse_set( &mut self, - set: NodeId, - systems: &[NodeId], + set: SystemSetKey, + systems: &[SystemKey], dependency_flattened: &DiGraph, dependencies_to_add: &mut Vec<(NodeId, NodeId)>, ) { diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index a754f1e1d4..0384377ab9 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,23 +11,24 @@ 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; +use slotmap::{new_key_type, SecondaryMap, SlotMap}; use thiserror::Error; #[cfg(feature = "trace")] use tracing::info_span; +use crate::component::CheckChangeTicks; use crate::{ - component::{ComponentId, Components, Tick}, - error::default_error_handler, + component::{ComponentId, Components}, prelude::Component, + query::FilteredAccessSet, resource::Resource, schedule::*, system::ScheduleSystem, @@ -112,7 +112,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( @@ -127,7 +127,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); } } @@ -167,7 +167,7 @@ impl Schedules { writeln!(message, "{}", components.get_name(*id).unwrap()).unwrap(); } - info!("{}", message); + info!("{message}"); } /// Adds one or more systems to the [`Schedule`] matching the provided [`ScheduleLabel`]. @@ -236,6 +236,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) { @@ -258,8 +259,16 @@ impl Chain { /// A collection of systems, and the metadata and executor needed to run them /// in a certain order under certain conditions. /// +/// # Schedule labels +/// +/// Each schedule has a [`ScheduleLabel`] value. This value is used to uniquely identify the +/// schedule when added to a [`World`]’s [`Schedules`], and may be used to specify which schedule +/// a system should be added to. +/// /// # Example +/// /// Here is an example of a `Schedule` running a "Hello world" system: +/// /// ``` /// # use bevy_ecs::prelude::*; /// fn hello_world() { println!("Hello world!") } @@ -274,6 +283,7 @@ impl Chain { /// ``` /// /// A schedule can also run several systems in an ordered way: +/// /// ``` /// # use bevy_ecs::prelude::*; /// fn system_one() { println!("System 1 works!") } @@ -292,6 +302,32 @@ impl Chain { /// schedule.run(&mut world); /// } /// ``` +/// +/// Schedules are often inserted into a [`World`] and identified by their [`ScheduleLabel`] only: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// use bevy_ecs::schedule::ScheduleLabel; +/// +/// // Declare a new schedule label. +/// #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] +/// struct Update; +/// +/// // This system shall be part of the schedule. +/// fn an_update_system() { +/// println!("Hello world!"); +/// } +/// +/// fn main() { +/// let mut world = World::new(); +/// +/// // Add a system to the schedule with that label (creating it automatically). +/// world.get_resource_or_init::().add_systems(Update, an_update_system); +/// +/// // Run the schedule, and therefore run the system. +/// world.run_schedule(Update); +/// } +/// ``` pub struct Schedule { label: InternedScheduleLabel, graph: ScheduleGraph, @@ -328,7 +364,8 @@ impl Schedule { this } - /// Get the `InternedScheduleLabel` for this `Schedule`. + /// Returns the [`InternedScheduleLabel`] for this `Schedule`, + /// corresponding to the [`ScheduleLabel`] this schedule was created with. pub fn label(&self) -> InternedScheduleLabel { self.label } @@ -368,7 +405,9 @@ impl Schedule { ); }; - self.graph.ambiguous_with.add_edge(a_id, b_id); + self.graph + .ambiguous_with + .add_edge(NodeId::Set(a_id), NodeId::Set(b_id)); self } @@ -395,9 +434,21 @@ impl Schedule { } /// Changes miscellaneous build settings. + /// + /// If [`settings.auto_insert_apply_deferred`][ScheduleBuildSettings::auto_insert_apply_deferred] + /// is `false`, this clears `*_ignore_deferred` edge settings configured so far. + /// + /// Generally this method should be used before adding systems or set configurations to the schedule, + /// not after. pub fn set_build_settings(&mut self, settings: ScheduleBuildSettings) -> &mut Self { if settings.auto_insert_apply_deferred { - self.add_build_pass(passes::AutoInsertApplyDeferredPass::default()); + if !self + .graph + .passes + .contains_key(&TypeId::of::()) + { + self.add_build_pass(passes::AutoInsertApplyDeferredPass::default()); + } } else { self.remove_build_pass::(); } @@ -442,7 +493,7 @@ impl Schedule { self.initialize(world) .unwrap_or_else(|e| panic!("Error when initializing schedule {:?}: {e}", self.label)); - let error_handler = default_error_handler(); + let error_handler = world.default_error_handler(); #[cfg(not(feature = "bevy_debug_stepping"))] self.executor @@ -511,22 +562,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(crate) 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); } } } @@ -540,7 +591,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); } } @@ -551,7 +602,7 @@ impl Schedule { /// schedule has never been initialized or run. pub fn systems( &self, - ) -> Result + Sized, ScheduleNotInitialized> + ) -> Result + Sized, ScheduleNotInitialized> { if !self.executor_initialized { return Err(ScheduleNotInitialized); @@ -562,7 +613,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) } @@ -630,30 +681,85 @@ 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() } } +new_key_type! { + /// A unique identifier for a system in a [`ScheduleGraph`]. + pub struct SystemKey; + /// A unique identifier for a system set in a [`ScheduleGraph`]. + pub struct SystemSetKey; +} + +enum UninitializedId { + System(SystemKey), + Set { + key: SystemSetKey, + first_uninit_condition: usize, + }, +} + /// Metadata for a [`Schedule`]. /// /// The order isn't optimized; calling `ScheduleGraph::build_schedule` will return a @@ -661,18 +767,18 @@ impl SystemNode { #[derive(Default)] pub struct ScheduleGraph { /// List of systems in the schedule - pub systems: Vec, + pub systems: SlotMap, /// List of conditions for each system, in the same order as `systems` - pub system_conditions: Vec>, + pub system_conditions: SecondaryMap>, /// List of system sets in the schedule - system_sets: Vec, + system_sets: SlotMap, /// List of conditions for each system set, in the same order as `system_sets` - system_set_conditions: Vec>, + system_set_conditions: SecondaryMap>, /// Map from system set to node id - system_set_ids: HashMap, + system_set_ids: HashMap, /// Systems that have not been initialized yet; for system sets, we store the index of the first uninitialized condition /// (all the conditions after that index still need to be initialized) - uninit: Vec<(NodeId, usize)>, + uninit: Vec, /// Directed acyclic graph of the hierarchy (which systems/sets are children of which sets) hierarchy: Dag, /// Directed acyclic graph of the dependency (which systems/sets have to run before which other systems/sets) @@ -680,7 +786,7 @@ pub struct ScheduleGraph { ambiguous_with: UnGraph, /// Nodes that are allowed to have ambiguous ordering relationship with any other systems. pub ambiguous_with_all: HashSet, - conflicting_systems: Vec<(NodeId, NodeId, Vec)>, + conflicting_systems: Vec<(SystemKey, SystemKey, Vec)>, anonymous_sets: usize, changed: bool, settings: ScheduleBuildSettings, @@ -692,10 +798,10 @@ impl ScheduleGraph { /// Creates an empty [`ScheduleGraph`] with default settings. pub fn new() -> Self { Self { - systems: Vec::new(), - system_conditions: Vec::new(), - system_sets: Vec::new(), - system_set_conditions: Vec::new(), + systems: SlotMap::with_key(), + system_conditions: SecondaryMap::new(), + system_sets: SlotMap::with_key(), + system_set_conditions: SecondaryMap::new(), system_set_ids: HashMap::default(), uninit: Vec::new(), hierarchy: Dag::new(), @@ -710,14 +816,12 @@ impl ScheduleGraph { } } - /// Returns the system at the given [`NodeId`], if it exists. - pub fn get_system_at(&self, id: NodeId) -> Option<&ScheduleSystem> { - if !id.is_system() { - return None; - } + /// Returns the system at the given [`SystemKey`], if it exists. + pub fn get_system_at(&self, key: SystemKey) -> Option<&ScheduleSystem> { self.systems - .get(id.index()) - .and_then(|system| system.inner.as_ref()) + .get(key) + .and_then(|system| system.get()) + .map(|system| &system.system) } /// Returns `true` if the given system set is part of the graph. Otherwise, returns `false`. @@ -729,70 +833,59 @@ impl ScheduleGraph { /// /// Panics if it doesn't exist. #[track_caller] - pub fn system_at(&self, id: NodeId) -> &ScheduleSystem { - self.get_system_at(id) - .ok_or_else(|| format!("system with id {id:?} does not exist in this Schedule")) - .unwrap() + pub fn system_at(&self, key: SystemKey) -> &ScheduleSystem { + self.get_system_at(key) + .unwrap_or_else(|| panic!("system with key {key:?} does not exist in this Schedule")) } /// Returns the set at the given [`NodeId`], if it exists. - pub fn get_set_at(&self, id: NodeId) -> Option<&dyn SystemSet> { - if !id.is_set() { - return None; - } - self.system_sets.get(id.index()).map(|set| &*set.inner) + pub fn get_set_at(&self, key: SystemSetKey) -> Option<&dyn SystemSet> { + self.system_sets.get(key).map(|set| &*set.inner) } /// Returns the set at the given [`NodeId`]. /// /// Panics if it doesn't exist. #[track_caller] - pub fn set_at(&self, id: NodeId) -> &dyn SystemSet { + pub fn set_at(&self, id: SystemSetKey) -> &dyn SystemSet { self.get_set_at(id) - .ok_or_else(|| format!("set with id {id:?} does not exist in this Schedule")) - .unwrap() + .unwrap_or_else(|| panic!("set with id {id:?} does not exist in this Schedule")) } - /// Returns the conditions for the set at the given [`NodeId`], if it exists. - pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[BoxedCondition]> { - if !id.is_set() { - return None; - } - self.system_set_conditions - .get(id.index()) - .map(Vec::as_slice) + /// Returns the conditions for the set at the given [`SystemSetKey`], if it exists. + pub fn get_set_conditions_at(&self, key: SystemSetKey) -> Option<&[ConditionWithAccess]> { + self.system_set_conditions.get(key).map(Vec::as_slice) } - /// Returns the conditions for the set at the given [`NodeId`]. + /// Returns the conditions for the set at the given [`SystemSetKey`]. /// /// Panics if it doesn't exist. #[track_caller] - pub fn set_conditions_at(&self, id: NodeId) -> &[BoxedCondition] { - self.get_set_conditions_at(id) - .ok_or_else(|| format!("set with id {id:?} does not exist in this Schedule")) - .unwrap() + pub fn set_conditions_at(&self, key: SystemSetKey) -> &[ConditionWithAccess] { + self.get_set_conditions_at(key) + .unwrap_or_else(|| panic!("set with key {key:?} does not exist in this Schedule")) } /// Returns an iterator over all systems in this schedule, along with the conditions for each system. - 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()?; - Some((NodeId::System(i), system, condition.as_slice())) - }) + pub fn systems( + &self, + ) -> impl Iterator { + self.systems.iter().filter_map(|(key, system_node)| { + let system = &system_node.inner.as_ref()?.system; + let conditions = self.system_conditions.get(key)?; + Some((key, system, conditions.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 { - self.system_set_ids.iter().map(|(_, &node_id)| { - let set_node = &self.system_sets[node_id.index()]; + pub fn system_sets( + &self, + ) -> impl Iterator { + self.system_sets.iter().filter_map(|(key, set_node)| { let set = &*set_node.inner; - let conditions = self.system_set_conditions[node_id.index()].as_slice(); - (node_id, set, conditions) + let conditions = self.system_set_conditions.get(key)?.as_slice(); + Some((key, set, conditions)) }) } @@ -816,7 +909,7 @@ impl ScheduleGraph { /// /// If the `Vec` is empty, the systems conflict on [`World`] access. /// Must be called after [`ScheduleGraph::build_schedule`] to be non-empty. - pub fn conflicting_systems(&self) -> &[(NodeId, NodeId, Vec)] { + pub fn conflicting_systems(&self) -> &[(SystemKey, SystemKey, Vec)] { &self.conflicting_systems } @@ -958,17 +1051,22 @@ impl ScheduleGraph { &mut self, config: ScheduleConfig, ) -> Result { - let id = NodeId::System(self.systems.len()); + let key = self.systems.insert(SystemNode::new(config.node)); + self.system_conditions.insert( + key, + config + .conditions + .into_iter() + .map(ConditionWithAccess::new) + .collect(), + ); + // system init has to be deferred (need `&mut World`) + self.uninit.push(UninitializedId::System(key)); // graph updates are immediate - self.update_graphs(id, config.metadata)?; + self.update_graphs(NodeId::System(key), config.metadata)?; - // 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); - - Ok(id) + Ok(NodeId::System(key)) } #[track_caller] @@ -984,52 +1082,33 @@ impl ScheduleGraph { let ScheduleConfig { node: set, metadata, - mut conditions, + conditions, } = set; - let id = match self.system_set_ids.get(&set) { + let key = match self.system_set_ids.get(&set) { Some(&id) => id, None => self.add_set(set), }; // graph updates are immediate - self.update_graphs(id, metadata)?; + self.update_graphs(NodeId::Set(key), metadata)?; // 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); + let system_set_conditions = self.system_set_conditions.entry(key).unwrap().or_default(); + self.uninit.push(UninitializedId::Set { + key, + first_uninit_condition: system_set_conditions.len(), + }); + system_set_conditions.extend(conditions.into_iter().map(ConditionWithAccess::new)); - Ok(id) + Ok(NodeId::Set(key)) } - fn add_set(&mut self, set: InternedSystemSet) -> NodeId { - let id = NodeId::Set(self.system_sets.len()); - self.system_sets.push(SystemSetNode::new(set)); - self.system_set_conditions.push(Vec::new()); - self.system_set_ids.insert(set, id); - id - } - - /// Checks that a system set isn't included in itself. - /// If not present, add the set to the graph. - fn check_hierarchy_set( - &mut self, - id: &NodeId, - set: InternedSystemSet, - ) -> Result<(), ScheduleBuildError> { - match self.system_set_ids.get(&set) { - Some(set_id) => { - if id == set_id { - return Err(ScheduleBuildError::HierarchyLoop(self.get_node_name(id))); - } - } - None => { - self.add_set(set); - } - } - - Ok(()) + fn add_set(&mut self, set: InternedSystemSet) -> SystemSetKey { + let key = self.system_sets.insert(SystemSetNode::new(set)); + self.system_set_conditions.insert(key, Vec::new()); + self.system_set_ids.insert(set, key); + key } fn create_anonymous_set(&mut self) -> AnonymousSet { @@ -1042,11 +1121,24 @@ impl ScheduleGraph { /// Add all the sets from the [`GraphInfo`]'s hierarchy to the graph. fn check_hierarchy_sets( &mut self, - id: &NodeId, + id: NodeId, graph_info: &GraphInfo, ) -> Result<(), ScheduleBuildError> { for &set in &graph_info.hierarchy { - self.check_hierarchy_set(id, set)?; + if let Some(&set_id) = self.system_set_ids.get(&set) { + if let NodeId::Set(key) = id + && set_id == key + { + { + return Err(ScheduleBuildError::HierarchyLoop( + self.get_node_name(&NodeId::Set(key)), + )); + } + } + } else { + // If the set is not in the graph, we add it + self.add_set(set); + } } Ok(()) @@ -1056,22 +1148,29 @@ impl ScheduleGraph { /// Add all the sets from the [`GraphInfo`]'s dependencies to the graph. fn check_edges( &mut self, - id: &NodeId, + id: NodeId, graph_info: &GraphInfo, ) -> Result<(), ScheduleBuildError> { for Dependency { set, .. } in &graph_info.dependencies { - match self.system_set_ids.get(set) { - Some(set_id) => { - if id == set_id { - return Err(ScheduleBuildError::DependencyLoop(self.get_node_name(id))); - } - } - None => { - self.add_set(*set); + if let Some(&set_id) = self.system_set_ids.get(set) { + if let NodeId::Set(key) = id + && set_id == key + { + return Err(ScheduleBuildError::DependencyLoop( + self.get_node_name(&NodeId::Set(key)), + )); } + } else { + // If the set is not in the graph, we add it + self.add_set(*set); } } + Ok(()) + } + + /// Add all the sets from the [`GraphInfo`]'s ambiguity to the graph. + fn add_ambiguities(&mut self, graph_info: &GraphInfo) { if let Ambiguity::IgnoreWithSet(ambiguous_with) = &graph_info.ambiguous_with { for set in ambiguous_with { if !self.system_set_ids.contains_key(set) { @@ -1079,8 +1178,6 @@ impl ScheduleGraph { } } } - - Ok(()) } /// Update the internal graphs (hierarchy, dependency, ambiguity) by adding a single [`GraphInfo`] @@ -1089,8 +1186,9 @@ impl ScheduleGraph { id: NodeId, graph_info: GraphInfo, ) -> Result<(), ScheduleBuildError> { - self.check_hierarchy_sets(&id, &graph_info)?; - self.check_edges(&id, &graph_info)?; + self.check_hierarchy_sets(id, &graph_info)?; + self.check_edges(id, &graph_info)?; + self.add_ambiguities(&graph_info); self.changed = true; let GraphInfo { @@ -1103,20 +1201,20 @@ impl ScheduleGraph { self.hierarchy.graph.add_node(id); self.dependency.graph.add_node(id); - for set in sets.into_iter().map(|set| self.system_set_ids[&set]) { - self.hierarchy.graph.add_edge(set, id); + for key in sets.into_iter().map(|set| self.system_set_ids[&set]) { + self.hierarchy.graph.add_edge(NodeId::Set(key), id); // ensure set also appears in dependency graph - self.dependency.graph.add_node(set); + self.dependency.graph.add_node(NodeId::Set(key)); } - for (kind, set, options) in dependencies + for (kind, key, options) in dependencies .into_iter() .map(|Dependency { kind, set, options }| (kind, self.system_set_ids[&set], options)) { let (lhs, rhs) = match kind { - DependencyKind::Before => (id, set), - DependencyKind::After => (set, id), + DependencyKind::Before => (id, NodeId::Set(key)), + DependencyKind::After => (NodeId::Set(key), id), }; self.dependency.graph.add_edge(lhs, rhs); for pass in self.passes.values_mut() { @@ -1124,17 +1222,17 @@ impl ScheduleGraph { } // ensure set also appears in hierarchy graph - self.hierarchy.graph.add_node(set); + self.hierarchy.graph.add_node(NodeId::Set(key)); } match ambiguous_with { Ambiguity::Check => (), Ambiguity::IgnoreWithSet(ambiguous_with) => { - for set in ambiguous_with + for key in ambiguous_with .into_iter() .map(|set| self.system_set_ids[&set]) { - self.ambiguous_with.add_edge(id, set); + self.ambiguous_with.add_edge(id, NodeId::Set(key)); } } Ambiguity::IgnoreAll => { @@ -1147,17 +1245,24 @@ impl ScheduleGraph { /// Initializes any newly-added systems and conditions by calling [`System::initialize`](crate::system::System) pub fn initialize(&mut self, world: &mut World) { - for (id, i) in self.uninit.drain(..) { + for id in self.uninit.drain(..) { match id { - NodeId::System(index) => { - self.systems[index].get_mut().unwrap().initialize(world); - for condition in &mut self.system_conditions[index] { - condition.initialize(world); + UninitializedId::System(key) => { + let system = self.systems[key].get_mut().unwrap(); + system.access = system.system.initialize(world); + for condition in &mut self.system_conditions[key] { + condition.access = condition.condition.initialize(world); } } - NodeId::Set(index) => { - for condition in self.system_set_conditions[index].iter_mut().skip(i) { - condition.initialize(world); + UninitializedId::Set { + key, + first_uninit_condition, + } => { + for condition in self.system_set_conditions[key] + .iter_mut() + .skip(first_uninit_condition) + { + condition.access = condition.condition.initialize(world); } } } @@ -1248,41 +1353,47 @@ impl ScheduleGraph { &self, hierarchy_topsort: &[NodeId], hierarchy_graph: &DiGraph, - ) -> (HashMap>, HashMap) { - let mut set_systems: HashMap> = + ) -> ( + HashMap>, + HashMap>, + ) { + let mut set_systems: HashMap> = HashMap::with_capacity_and_hasher(self.system_sets.len(), Default::default()); - let mut set_system_bitsets = + let mut set_system_sets: HashMap> = HashMap::with_capacity_and_hasher(self.system_sets.len(), Default::default()); for &id in hierarchy_topsort.iter().rev() { - if id.is_system() { + let NodeId::Set(set_key) = id else { continue; - } + }; let mut systems = Vec::new(); - let mut system_bitset = FixedBitSet::with_capacity(self.systems.len()); + let mut system_set = HashSet::with_capacity(self.systems.len()); for child in hierarchy_graph.neighbors_directed(id, Outgoing) { match child { - NodeId::System(_) => { - systems.push(child); - system_bitset.insert(child.index()); + NodeId::System(key) => { + systems.push(key); + system_set.insert(key); } - NodeId::Set(_) => { - let child_systems = set_systems.get(&child).unwrap(); - let child_system_bitset = set_system_bitsets.get(&child).unwrap(); + NodeId::Set(key) => { + let child_systems = set_systems.get(&key).unwrap(); + let child_system_set = set_system_sets.get(&key).unwrap(); systems.extend_from_slice(child_systems); - system_bitset.union_with(child_system_bitset); + system_set.extend(child_system_set.iter()); } } } - set_systems.insert(id, systems); - set_system_bitsets.insert(id, system_bitset); + set_systems.insert(set_key, systems); + set_system_sets.insert(set_key, system_set); } - (set_systems, set_system_bitsets) + (set_systems, set_system_sets) } - fn get_dependency_flattened(&mut self, set_systems: &HashMap>) -> DiGraph { + fn get_dependency_flattened( + &mut self, + set_systems: &HashMap>, + ) -> DiGraph { // flatten: combine `in_set` with `before` and `after` information // have to do it like this to preserve transitivity let mut dependency_flattened = self.dependency.graph.clone(); @@ -1293,26 +1404,26 @@ impl ScheduleGraph { } if systems.is_empty() { // collapse dependencies for empty sets - for a in dependency_flattened.neighbors_directed(set, Incoming) { - for b in dependency_flattened.neighbors_directed(set, Outgoing) { + for a in dependency_flattened.neighbors_directed(NodeId::Set(set), Incoming) { + for b in dependency_flattened.neighbors_directed(NodeId::Set(set), Outgoing) { temp.push((a, b)); } } } else { - for a in dependency_flattened.neighbors_directed(set, Incoming) { + for a in dependency_flattened.neighbors_directed(NodeId::Set(set), Incoming) { for &sys in systems { - temp.push((a, sys)); + temp.push((a, NodeId::System(sys))); } } - for b in dependency_flattened.neighbors_directed(set, Outgoing) { + for b in dependency_flattened.neighbors_directed(NodeId::Set(set), Outgoing) { for &sys in systems { - temp.push((sys, b)); + temp.push((NodeId::System(sys), b)); } } } - dependency_flattened.remove_node(set); + dependency_flattened.remove_node(NodeId::Set(set)); for (a, b) in temp.drain(..) { dependency_flattened.add_edge(a, b); } @@ -1321,27 +1432,31 @@ impl ScheduleGraph { dependency_flattened } - fn get_ambiguous_with_flattened(&self, set_systems: &HashMap>) -> UnGraph { + fn get_ambiguous_with_flattened( + &self, + set_systems: &HashMap>, + ) -> UnGraph { let mut ambiguous_with_flattened = UnGraph::default(); for (lhs, rhs) in self.ambiguous_with.all_edges() { match (lhs, rhs) { (NodeId::System(_), NodeId::System(_)) => { ambiguous_with_flattened.add_edge(lhs, rhs); } - (NodeId::Set(_), NodeId::System(_)) => { + (NodeId::Set(lhs), NodeId::System(_)) => { for &lhs_ in set_systems.get(&lhs).unwrap_or(&Vec::new()) { - ambiguous_with_flattened.add_edge(lhs_, rhs); + ambiguous_with_flattened.add_edge(NodeId::System(lhs_), rhs); } } - (NodeId::System(_), NodeId::Set(_)) => { + (NodeId::System(_), NodeId::Set(rhs)) => { for &rhs_ in set_systems.get(&rhs).unwrap_or(&Vec::new()) { - ambiguous_with_flattened.add_edge(lhs, rhs_); + ambiguous_with_flattened.add_edge(lhs, NodeId::System(rhs_)); } } - (NodeId::Set(_), NodeId::Set(_)) => { + (NodeId::Set(lhs), NodeId::Set(rhs)) => { for &lhs_ in set_systems.get(&lhs).unwrap_or(&Vec::new()) { for &rhs_ in set_systems.get(&rhs).unwrap_or(&vec![]) { - ambiguous_with_flattened.add_edge(lhs_, rhs_); + ambiguous_with_flattened + .add_edge(NodeId::System(lhs_), NodeId::System(rhs_)); } } } @@ -1356,7 +1471,7 @@ impl ScheduleGraph { flat_results_disconnected: &Vec<(NodeId, NodeId)>, ambiguous_with_flattened: &UnGraph, ignored_ambiguities: &BTreeSet, - ) -> Vec<(NodeId, NodeId, Vec)> { + ) -> Vec<(SystemKey, SystemKey, Vec)> { let mut conflicting_systems = Vec::new(); for &(a, b) in flat_results_disconnected { if ambiguous_with_flattened.contains_edge(a, b) @@ -1366,13 +1481,23 @@ impl ScheduleGraph { continue; } - 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() { + let NodeId::System(a) = a else { + panic!( + "Encountered a non-system node in the flattened disconnected results: {a:?}" + ); + }; + let NodeId::System(b) = b else { + panic!( + "Encountered a non-system node in the flattened disconnected results: {b:?}" + ); + }; + let system_a = self.systems[a].get().unwrap(); + let system_b = self.systems[b].get().unwrap(); + 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(); - let access_b = system_b.component_access(); + 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) => { @@ -1403,7 +1528,11 @@ impl ScheduleGraph { dependency_flattened_dag: Dag, hier_results_reachable: FixedBitSet, ) -> SystemSchedule { - let dg_system_ids = dependency_flattened_dag.topsort.clone(); + let dg_system_ids = dependency_flattened_dag + .topsort + .iter() + .filter_map(NodeId::as_system) + .collect::>(); let dg_system_idx_map = dg_system_ids .iter() .cloned() @@ -1417,7 +1546,7 @@ impl ScheduleGraph { .iter() .cloned() .enumerate() - .filter(|&(_i, id)| id.is_system()) + .filter_map(|(i, id)| Some((i, id.as_system()?))) .collect::>(); let (hg_set_with_conditions_idxs, hg_set_ids): (Vec<_>, Vec<_>) = self @@ -1426,10 +1555,11 @@ impl ScheduleGraph { .iter() .cloned() .enumerate() - .filter(|&(_i, id)| { + .filter_map(|(i, id)| { // ignore system sets that have no conditions // ignore system type sets (already covered, they don't have conditions) - id.is_set() && !self.system_set_conditions[id.index()].is_empty() + let key = id.as_set()?; + (!self.system_set_conditions[key].is_empty()).then_some((i, key)) }) .unzip(); @@ -1441,16 +1571,19 @@ impl ScheduleGraph { // (needed by multi_threaded executor to run systems in the correct order) let mut system_dependencies = Vec::with_capacity(sys_count); let mut system_dependents = Vec::with_capacity(sys_count); - for &sys_id in &dg_system_ids { + for &sys_key in &dg_system_ids { let num_dependencies = dependency_flattened_dag .graph - .neighbors_directed(sys_id, Incoming) + .neighbors_directed(NodeId::System(sys_key), Incoming) .count(); let dependents = dependency_flattened_dag .graph - .neighbors_directed(sys_id, Outgoing) - .map(|dep_id| dg_system_idx_map[&dep_id]) + .neighbors_directed(NodeId::System(sys_key), Outgoing) + .filter_map(|dep_id| { + let dep_key = dep_id.as_system()?; + Some(dg_system_idx_map[&dep_key]) + }) .collect::>(); system_dependencies.push(num_dependencies); @@ -1463,8 +1596,8 @@ impl ScheduleGraph { vec![FixedBitSet::with_capacity(sys_count); set_with_conditions_count]; for (i, &row) in hg_set_with_conditions_idxs.iter().enumerate() { let bitset = &mut systems_in_sets_with_conditions[i]; - for &(col, sys_id) in &hg_systems { - let idx = dg_system_idx_map[&sys_id]; + for &(col, sys_key) in &hg_systems { + let idx = dg_system_idx_map[&sys_key]; let is_descendant = hier_results_reachable[index(row, col, hg_node_count)]; bitset.set(idx, is_descendant); } @@ -1472,8 +1605,8 @@ impl ScheduleGraph { let mut sets_with_conditions_of_systems = vec![FixedBitSet::with_capacity(set_with_conditions_count); sys_count]; - for &(col, sys_id) in &hg_systems { - let i = dg_system_idx_map[&sys_id]; + for &(col, sys_key) in &hg_systems { + let i = dg_system_idx_map[&sys_key]; let bitset = &mut sets_with_conditions_of_systems[i]; for (idx, &row) in hg_set_with_conditions_idxs .iter() @@ -1511,36 +1644,36 @@ impl ScheduleGraph { } // move systems out of old schedule - for ((id, system), conditions) in schedule + for ((key, system), conditions) in schedule .system_ids .drain(..) .zip(schedule.systems.drain(..)) .zip(schedule.system_conditions.drain(..)) { - self.systems[id.index()].inner = Some(system); - self.system_conditions[id.index()] = conditions; + self.systems[key].inner = Some(system); + self.system_conditions[key] = conditions; } - for (id, conditions) in schedule + for (key, conditions) in schedule .set_ids .drain(..) .zip(schedule.set_conditions.drain(..)) { - self.system_set_conditions[id.index()] = conditions; + self.system_set_conditions[key] = conditions; } *schedule = self.build_schedule(world, schedule_label, ignored_ambiguities)?; // move systems into new schedule - for &id in &schedule.system_ids { - let system = self.systems[id.index()].inner.take().unwrap(); - let conditions = core::mem::take(&mut self.system_conditions[id.index()]); + for &key in &schedule.system_ids { + let system = self.systems[key].inner.take().unwrap(); + let conditions = core::mem::take(&mut self.system_conditions[key]); schedule.systems.push(system); schedule.system_conditions.push(conditions); } - for &id in &schedule.set_ids { - let conditions = core::mem::take(&mut self.system_set_conditions[id.index()]); + for &key in &schedule.set_ids { + let conditions = core::mem::take(&mut self.system_set_conditions[key]); schedule.set_conditions.push(conditions); } @@ -1593,9 +1726,14 @@ impl ScheduleGraph { #[inline] fn get_node_name_inner(&self, id: &NodeId, report_sets: bool) -> String { - let name = match id { - NodeId::System(_) => { - let name = self.systems[id.index()].get().unwrap().name().to_string(); + match *id { + NodeId::System(key) => { + let name = self.systems[key].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() { @@ -1609,19 +1747,14 @@ impl ScheduleGraph { name } } - NodeId::Set(_) => { - let set = &self.system_sets[id.index()]; + NodeId::Set(key) => { + let set = &self.system_sets[key]; if set.is_anonymous() { self.anonymous_set_name(id) } else { set.name() } } - }; - if self.settings.use_shortnames { - ShortName(&name).to_string() - } else { - name } } @@ -1660,10 +1793,7 @@ impl ScheduleGraph { match self.settings.hierarchy_detection { LogLevel::Ignore => unreachable!(), LogLevel::Warn => { - error!( - "Schedule {schedule_label:?} has redundant edges:\n {}", - message - ); + error!("Schedule {schedule_label:?} has redundant edges:\n {message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::HierarchyRedundancy(message)), @@ -1806,16 +1936,16 @@ impl ScheduleGraph { fn check_order_but_intersect( &self, dep_results_connected: &HashSet<(NodeId, NodeId)>, - set_system_bitsets: &HashMap, + set_system_sets: &HashMap>, ) -> Result<(), ScheduleBuildError> { // check that there is no ordering between system sets that intersect for (a, b) in dep_results_connected { - if !(a.is_set() && b.is_set()) { + let (NodeId::Set(a_key), NodeId::Set(b_key)) = (a, b) else { continue; - } + }; - let a_systems = set_system_bitsets.get(a).unwrap(); - let b_systems = set_system_bitsets.get(b).unwrap(); + let a_systems = set_system_sets.get(a_key).unwrap(); + let b_systems = set_system_sets.get(b_key).unwrap(); if !a_systems.is_disjoint(b_systems) { return Err(ScheduleBuildError::SetsHaveOrderButIntersect( @@ -1830,19 +1960,25 @@ impl ScheduleGraph { fn check_system_type_set_ambiguity( &self, - set_systems: &HashMap>, + set_systems: &HashMap>, ) -> Result<(), ScheduleBuildError> { - for (&id, systems) in set_systems { - let set = &self.system_sets[id.index()]; + for (&key, systems) in set_systems { + let set = &self.system_sets[key]; if set.is_system_type() { let instances = systems.len(); - let ambiguous_with = self.ambiguous_with.edges(id); - let before = self.dependency.graph.edges_directed(id, Incoming); - let after = self.dependency.graph.edges_directed(id, Outgoing); + let ambiguous_with = self.ambiguous_with.edges(NodeId::Set(key)); + let before = self + .dependency + .graph + .edges_directed(NodeId::Set(key), Incoming); + let after = self + .dependency + .graph + .edges_directed(NodeId::Set(key), Outgoing); let relations = before.count() + after.count() + ambiguous_with.count(); if instances > 1 && relations > 0 { return Err(ScheduleBuildError::SystemTypeSetAmbiguity( - self.get_node_name(&id), + self.get_node_name(&NodeId::Set(key)), )); } } @@ -1853,7 +1989,7 @@ impl ScheduleGraph { /// if [`ScheduleBuildSettings::ambiguity_detection`] is [`LogLevel::Ignore`], this check is skipped fn optionally_check_conflicts( &self, - conflicts: &[(NodeId, NodeId, Vec)], + conflicts: &[(SystemKey, SystemKey, Vec)], components: &Components, schedule_label: InternedScheduleLabel, ) -> Result<(), ScheduleBuildError> { @@ -1865,7 +2001,7 @@ impl ScheduleGraph { match self.settings.ambiguity_detection { LogLevel::Ignore => Ok(()), LogLevel::Warn => { - warn!("Schedule {schedule_label:?} has ambiguities.\n{}", message); + warn!("Schedule {schedule_label:?} has ambiguities.\n{message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::Ambiguity(message)), @@ -1874,7 +2010,7 @@ impl ScheduleGraph { fn get_conflicts_error_message( &self, - ambiguities: &[(NodeId, NodeId, Vec)], + ambiguities: &[(SystemKey, SystemKey, Vec)], components: &Components, ) -> String { let n_ambiguities = ambiguities.len(); @@ -1902,17 +2038,14 @@ impl ScheduleGraph { /// convert conflicts to human readable format pub fn conflicts_to_string<'a>( &'a self, - ambiguities: &'a [(NodeId, NodeId, Vec)], + ambiguities: &'a [(SystemKey, SystemKey, Vec)], components: &'a Components, - ) -> impl Iterator>)> + 'a { + ) -> impl Iterator)> + 'a { ambiguities .iter() .map(move |(system_a, system_b, conflicts)| { - let name_a = self.get_node_name(system_a); - let name_b = self.get_node_name(system_b); - - debug_assert!(system_a.is_system(), "{name_a} is not a system."); - debug_assert!(system_b.is_system(), "{name_b} is not a system."); + let name_a = self.get_node_name(&NodeId::System(*system_a)); + let name_b = self.get_node_name(&NodeId::System(*system_b)); let conflict_names: Vec<_> = conflicts .iter() @@ -1923,22 +2056,25 @@ impl ScheduleGraph { }) } - fn traverse_sets_containing_node(&self, id: NodeId, f: &mut impl FnMut(NodeId) -> bool) { + fn traverse_sets_containing_node(&self, id: NodeId, f: &mut impl FnMut(SystemSetKey) -> bool) { for (set_id, _) in self.hierarchy.graph.edges_directed(id, Incoming) { - if f(set_id) { - self.traverse_sets_containing_node(set_id, f); + let NodeId::Set(set_key) = set_id else { + continue; + }; + if f(set_key) { + self.traverse_sets_containing_node(NodeId::Set(set_key), f); } } } fn names_of_sets_containing_node(&self, id: &NodeId) -> Vec { let mut sets = >::default(); - self.traverse_sets_containing_node(*id, &mut |set_id| { - !self.system_sets[set_id.index()].is_system_type() && sets.insert(set_id) + self.traverse_sets_containing_node(*id, &mut |key| { + !self.system_sets[key].is_system_type() && sets.insert(key) }); let mut sets: Vec<_> = sets .into_iter() - .map(|set_id| self.get_node_name(&set_id)) + .map(|key| self.get_node_name(&NodeId::Set(key))) .collect(); sets.sort(); sets @@ -2061,6 +2197,7 @@ mod tests { use bevy_ecs_macros::ScheduleLabel; use crate::{ + error::{ignore, panic, DefaultErrorHandler, Result}, prelude::{ApplyDeferred, Res, Resource}, schedule::{ tests::ResMut, IntoScheduleConfigs, Schedule, ScheduleBuildSettings, SystemSet, @@ -2077,6 +2214,46 @@ mod tests { #[derive(Resource)] struct Resource2; + #[test] + fn unchanged_auto_insert_apply_deferred_has_no_effect() { + use alloc::{vec, vec::Vec}; + + #[derive(PartialEq, Debug)] + enum Entry { + System(usize), + SyncPoint(usize), + } + + #[derive(Resource, Default)] + struct Log(Vec); + + fn system(mut res: ResMut, mut commands: Commands) { + res.0.push(Entry::System(N)); + commands + .queue(|world: &mut World| world.resource_mut::().0.push(Entry::SyncPoint(N))); + } + + let mut world = World::default(); + world.init_resource::(); + let mut schedule = Schedule::default(); + schedule.add_systems((system::<1>, system::<2>).chain_ignore_deferred()); + schedule.set_build_settings(ScheduleBuildSettings { + auto_insert_apply_deferred: true, + ..Default::default() + }); + schedule.run(&mut world); + let actual = world.remove_resource::().unwrap().0; + + let expected = vec![ + Entry::System(1), + Entry::System(2), + Entry::SyncPoint(1), + Entry::SyncPoint(2), + ]; + + assert_eq!(actual, expected); + } + // regression test for https://github.com/bevyengine/bevy/issues/9114 #[test] fn ambiguous_with_not_breaking_run_conditions() { @@ -2810,4 +2987,32 @@ mod tests { .expect("CheckSystemRan Resource Should Exist"); assert_eq!(value.0, 2); } + + #[test] + fn test_default_error_handler() { + #[derive(Resource, Default)] + struct Ran(bool); + + fn system(mut ran: ResMut) -> Result { + ran.0 = true; + Err("I failed!".into()) + } + + // Test that the default error handler is used + let mut world = World::default(); + world.init_resource::(); + world.insert_resource(DefaultErrorHandler(ignore)); + let mut schedule = Schedule::default(); + schedule.add_systems(system).run(&mut world); + assert!(world.resource::().0); + + // Test that the handler doesn't change within the schedule + schedule.add_systems( + (|world: &mut World| { + world.insert_resource(DefaultErrorHandler(panic)); + }) + .before(system), + ); + schedule.run(&mut world); + } } diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 896c7ed050..b0a3e95cb7 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, @@ -13,13 +14,45 @@ use crate::{ define_label, intern::Interned, system::{ - ExclusiveFunctionSystem, ExclusiveSystemParamFunction, FunctionSystem, + ExclusiveFunctionSystem, ExclusiveSystemParamFunction, FunctionSystem, IntoResult, IsExclusiveFunctionSystem, IsFunctionSystem, SystemParamFunction, }, }; define_label!( - /// A strongly-typed class of labels used to identify a [`Schedule`](crate::schedule::Schedule). + /// A strongly-typed class of labels used to identify a [`Schedule`]. + /// + /// Each schedule in a [`World`] has a unique schedule label value, and + /// schedules can be automatically created from labels via [`Schedules::add_systems()`]. + /// + /// # Defining new schedule labels + /// + /// By default, you should use Bevy's premade schedule labels which implement this trait. + /// If you are using [`bevy_ecs`] directly or if you need to run a group of systems outside + /// the existing schedules, you may define your own schedule labels by using + /// `#[derive(ScheduleLabel)]`. + /// + /// ``` + /// use bevy_ecs::prelude::*; + /// use bevy_ecs::schedule::ScheduleLabel; + /// + /// // Declare a new schedule label. + /// #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] + /// struct Update; + /// + /// let mut world = World::new(); + /// + /// // Add a system to the schedule with that label (creating it automatically). + /// fn a_system_function() {} + /// world.get_resource_or_init::().add_systems(Update, a_system_function); + /// + /// // Run the schedule, and therefore run the system. + /// world.run_schedule(Update); + /// ``` + /// + /// [`Schedule`]: crate::schedule::Schedule + /// [`Schedules::add_systems()`]: crate::schedule::Schedules::add_systems + /// [`World`]: crate::world::World #[diagnostic::on_unimplemented( note = "consider annotating `{Self}` with `#[derive(ScheduleLabel)]`" )] @@ -28,7 +61,93 @@ define_label!( ); define_label!( - /// Types that identify logical groups of systems. + /// System sets are tag-like labels that can be used to group systems together. + /// + /// This allows you to share configuration (like run conditions) across multiple systems, + /// and order systems or system sets relative to conceptual groups of systems. + /// To control the behavior of a system set as a whole, use [`Schedule::configure_sets`](crate::prelude::Schedule::configure_sets), + /// or the method of the same name on `App`. + /// + /// Systems can belong to any number of system sets, reflecting multiple roles or facets that they might have. + /// For example, you may want to annotate a system as "consumes input" and "applies forces", + /// and ensure that your systems are ordered correctly for both of those sets. + /// + /// System sets can belong to any number of other system sets, + /// allowing you to create nested hierarchies of system sets to group systems together. + /// Configuration applied to system sets will flow down to their members (including other system sets), + /// allowing you to set and modify the configuration in a single place. + /// + /// Systems sets are also useful for exposing a consistent public API for dependencies + /// to hook into across versions of your crate, + /// allowing them to add systems to a specific set, or order relative to that set, + /// without leaking implementation details of the exact systems involved. + /// + /// ## Defining new system sets + /// + /// To create a new system set, use the `#[derive(SystemSet)]` macro. + /// Unit structs are a good choice for one-off sets. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// struct PhysicsSystems; + /// ``` + /// + /// When you want to define several related system sets, + /// consider creating an enum system set. + /// Each variant will be treated as a separate system set. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// ``` + /// + /// By convention, the listed order of the system set in the enum + /// corresponds to the order in which the systems are run. + /// Ordering must be explicitly added to ensure that this is the case, + /// but following this convention will help avoid confusion. + /// + /// ### Adding systems to system sets + /// + /// To add systems to a system set, call [`in_set`](crate::prelude::IntoScheduleConfigs::in_set) on the system function + /// while adding it to your app or schedule. + /// + /// Like usual, these methods can be chained with other configuration methods like [`before`](crate::prelude::IntoScheduleConfigs::before), + /// or repeated to add systems to multiple sets. + /// + /// ```rust + /// use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// + /// fn target_selection() {} + /// + /// fn enemy_damage_calculation() {} + /// + /// fn player_damage_calculation() {} + /// + /// let mut schedule = Schedule::default(); + /// // Configuring the sets to run in order. + /// schedule.configure_sets((CombatSystems::TargetSelection, CombatSystems::DamageCalculation, CombatSystems::Cleanup).chain()); + /// + /// // Adding a single system to a set. + /// schedule.add_systems(target_selection.in_set(CombatSystems::TargetSelection)); + /// + /// // Adding multiple systems to a set. + /// schedule.add_systems((player_damage_calculation, enemy_damage_calculation).in_set(CombatSystems::DamageCalculation)); + /// ``` #[diagnostic::on_unimplemented( note = "consider annotating `{Self}` with `#[derive(SystemSet)]`" )] @@ -78,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() } } @@ -115,15 +234,6 @@ impl SystemSet for SystemTypeSet { fn dyn_clone(&self) -> Box { Box::new(*self) } - - fn as_dyn_eq(&self) -> &dyn DynEq { - self - } - - fn dyn_hash(&self, mut state: &mut dyn Hasher) { - TypeId::of::().hash(&mut state); - self.hash(&mut state); - } } /// A [`SystemSet`] implicitly created when using @@ -146,15 +256,6 @@ impl SystemSet for AnonymousSet { fn dyn_clone(&self) -> Box { Box::new(*self) } - - fn as_dyn_eq(&self) -> &dyn DynEq { - self - } - - fn dyn_hash(&self, mut state: &mut dyn Hasher) { - TypeId::of::().hash(&mut state); - self.hash(&mut state); - } } /// Types that can be converted into a [`SystemSet`]. @@ -190,13 +291,14 @@ impl IntoSystemSet<()> for S { impl IntoSystemSet<(IsFunctionSystem, Marker)> for F where Marker: 'static, + F::Out: IntoResult<()>, F: SystemParamFunction, { - type Set = SystemTypeSet>; + type Set = SystemTypeSet>; #[inline] fn into_system_set(self) -> Self::Set { - SystemTypeSet::>::new() + SystemTypeSet::>::new() } } @@ -204,13 +306,14 @@ where impl IntoSystemSet<(IsExclusiveFunctionSystem, Marker)> for F where Marker: 'static, + F::Out: IntoResult<()>, F: ExclusiveSystemParamFunction, { - type Set = SystemTypeSet>; + type Set = SystemTypeSet>; #[inline] fn into_system_set(self) -> Self::Set { - SystemTypeSet::>::new() + SystemTypeSet::>::new() } } @@ -219,6 +322,7 @@ mod tests { use crate::{ resource::Resource, schedule::{tests::ResMut, Schedule}, + system::{IntoSystem, System}, }; use super::*; @@ -445,4 +549,22 @@ mod tests { GenericSet::(PhantomData).intern() ); } + + #[test] + fn system_set_matches_default_system_set() { + fn system() {} + let set_from_into_system_set = IntoSystemSet::into_system_set(system).intern(); + let system = IntoSystem::into_system(system); + let set_from_system = system.default_system_sets()[0]; + assert_eq!(set_from_into_system_set, set_from_system); + } + + #[test] + fn system_set_matches_default_system_set_exclusive() { + fn system(_: &mut crate::world::World) {} + let set_from_into_system_set = IntoSystemSet::into_system_set(system).intern(); + let system = IntoSystem::into_system(system); + let set_from_system = system.default_system_sets()[0]; + assert_eq!(set_from_into_system_set, set_from_system); + } } diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index b5df8555e2..d765ebe060 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -1,6 +1,6 @@ use crate::{ resource::Resource, - schedule::{InternedScheduleLabel, NodeId, Schedule, ScheduleLabel}, + schedule::{InternedScheduleLabel, NodeId, Schedule, ScheduleLabel, SystemKey}, system::{IntoSystem, ResMut}, }; use alloc::vec::Vec; @@ -125,7 +125,7 @@ impl core::fmt::Debug for Stepping { if self.action != Action::RunAll { let Cursor { schedule, system } = self.cursor; match self.schedule_order.get(schedule) { - Some(label) => write!(f, "cursor: {:?}[{}], ", label, system)?, + Some(label) => write!(f, "cursor: {label:?}[{system}], ")?, None => write!(f, "cursor: None, ")?, }; } @@ -173,7 +173,7 @@ impl Stepping { state .node_ids .get(self.cursor.system) - .map(|node_id| (*label, *node_id)) + .map(|node_id| (*label, NodeId::System(*node_id))) } /// Enable stepping for the provided schedule @@ -475,9 +475,8 @@ impl Stepping { Some(state) => state.clear_behaviors(), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } }, @@ -486,9 +485,8 @@ impl Stepping { Some(state) => state.set_behavior(system, behavior), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -498,9 +496,8 @@ impl Stepping { Some(state) => state.clear_behavior(system), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -609,7 +606,7 @@ struct ScheduleState { /// This is a cached copy of `SystemExecutable::system_ids`. We need it /// available here to be accessed by [`Stepping::cursor()`] so we can return /// [`NodeId`]s to the caller. - node_ids: Vec, + node_ids: Vec, /// changes to system behavior that should be applied the next time /// [`ScheduleState::skipped_systems()`] is called @@ -665,15 +662,15 @@ impl ScheduleState { // updates for the system TypeId. // PERF: If we add a way to efficiently query schedule systems by their TypeId, we could remove the full // system scan here - for (node_id, system) in schedule.systems().unwrap() { + for (key, system) in schedule.systems().unwrap() { let behavior = self.behavior_updates.get(&system.type_id()); match behavior { None => continue, Some(None) => { - self.behaviors.remove(&node_id); + self.behaviors.remove(&NodeId::System(key)); } Some(Some(behavior)) => { - self.behaviors.insert(node_id, *behavior); + self.behaviors.insert(NodeId::System(key), *behavior); } } } @@ -706,8 +703,8 @@ impl ScheduleState { // if we don't have a first system set, set it now if self.first.is_none() { - for (i, (node_id, _)) in schedule.systems().unwrap().enumerate() { - match self.behaviors.get(&node_id) { + for (i, (key, _)) in schedule.systems().unwrap().enumerate() { + match self.behaviors.get(&NodeId::System(key)) { Some(SystemBehavior::AlwaysRun | SystemBehavior::NeverRun) => continue, Some(_) | None => { self.first = Some(i); @@ -720,10 +717,10 @@ impl ScheduleState { let mut skip = FixedBitSet::with_capacity(schedule.systems_len()); let mut pos = start; - for (i, (node_id, _system)) in schedule.systems().unwrap().enumerate() { + for (i, (key, _system)) in schedule.systems().unwrap().enumerate() { let behavior = self .behaviors - .get(&node_id) + .get(&NodeId::System(key)) .unwrap_or(&SystemBehavior::Continue); #[cfg(test)] @@ -828,6 +825,7 @@ mod tests { use super::*; use crate::{prelude::*, schedule::ScheduleLabel}; use alloc::{format, vec}; + use slotmap::SlotMap; use std::println; #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] @@ -898,9 +896,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(); @@ -1470,10 +1468,11 @@ mod tests { // helper to build a cursor tuple for the supplied schedule fn cursor(schedule: &Schedule, index: usize) -> (InternedScheduleLabel, NodeId) { let node_id = schedule.executable().system_ids[index]; - (schedule.label(), node_id) + (schedule.label(), NodeId::System(node_id)) } let mut world = World::new(); + let mut slotmap = SlotMap::::with_key(); // create two schedules with a number of systems in them let mut schedule_a = Schedule::new(TestScheduleA); @@ -1520,6 +1519,11 @@ mod tests { ] ); + let sys0 = slotmap.insert(()); + let sys1 = slotmap.insert(()); + let _sys2 = slotmap.insert(()); + let sys3 = slotmap.insert(()); + // reset our cursor (disable/enable), and update stepping to test if the // cursor properly skips over AlwaysRun & NeverRun systems. Also set // a Break system to ensure that shows properly in the cursor @@ -1527,9 +1531,9 @@ mod tests { // disable/enable to reset cursor .disable() .enable() - .set_breakpoint_node(TestScheduleA, NodeId::System(1)) - .always_run_node(TestScheduleA, NodeId::System(3)) - .never_run_node(TestScheduleB, NodeId::System(0)); + .set_breakpoint_node(TestScheduleA, NodeId::System(sys1)) + .always_run_node(TestScheduleA, NodeId::System(sys3)) + .never_run_node(TestScheduleB, NodeId::System(sys0)); let mut cursors = Vec::new(); for _ in 0..9 { diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index d5014f2240..8e1a019222 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -199,17 +199,8 @@ unsafe impl + Send + Sync + 'static> Bundle ) { ::get_component_ids(components, ids); } - - fn register_required_components( - components: &mut crate::component::ComponentsRegistrator, - required_components: &mut crate::component::RequiredComponents, - ) { - ::register_required_components( - components, - required_components, - ); - } } + impl> DynamicBundle for SpawnRelatedBundle { type Effect = Self; @@ -266,16 +257,6 @@ unsafe impl Bundle for SpawnOneRelated { ) { ::get_component_ids(components, ids); } - - fn register_required_components( - components: &mut crate::component::ComponentsRegistrator, - required_components: &mut crate::component::RequiredComponents, - ) { - ::register_required_components( - components, - required_components, - ); - } } /// [`RelationshipTarget`] methods that create a [`Bundle`] with a [`DynamicBundle::Effect`] that: diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index caa0785b79..1a90cc511c 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,11 +1,10 @@ use crate::{ - archetype::ArchetypeComponentId, 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")] @@ -24,8 +23,7 @@ pub struct ResourceData { not(feature = "std"), expect(dead_code, reason = "currently only used with the std feature") )] - type_name: String, - id: ArchetypeComponentId, + type_name: DebugName, #[cfg(feature = "std")] origin_thread_id: Option, changed_by: MaybeLocation>>, @@ -66,32 +64,23 @@ impl ResourceData { /// If `SEND` is false, this will panic if called from a different thread than the one it was inserted from. #[inline] fn validate_access(&self) { - if SEND { - #[cfg_attr( - not(feature = "std"), - expect( - clippy::needless_return, - reason = "needless until no_std is addressed (see below)", - ) - )] - return; - } + if !SEND { + #[cfg(feature = "std")] + if self.origin_thread_id != Some(std::thread::current().id()) { + // Panic in tests, as testing for aborting is nearly impossible + panic!( + "Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.", + self.type_name, + self.origin_thread_id, + std::thread::current().id() + ); + } - #[cfg(feature = "std")] - if self.origin_thread_id != Some(std::thread::current().id()) { - // Panic in tests, as testing for aborting is nearly impossible - panic!( - "Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.", - self.type_name, - self.origin_thread_id, - std::thread::current().id() - ); + // TODO: Handle no_std non-send. + // Currently, no_std is single-threaded only, so this is safe to ignore. + // To support no_std multithreading, an alternative will be required. + // Remove the #[expect] attribute above when this is addressed. } - - // TODO: Handle no_std non-send. - // Currently, no_std is single-threaded only, so this is safe to ignore. - // To support no_std multithreading, an alternative will be required. - // Remove the #[expect] attribute above when this is addressed. } /// Returns true if the resource is populated. @@ -100,12 +89,6 @@ impl ResourceData { !self.data.is_empty() } - /// Gets the [`ArchetypeComponentId`] for the resource. - #[inline] - pub fn id(&self) -> ArchetypeComponentId { - self.id - } - /// Returns a reference to the resource, if it exists. /// /// # Panics @@ -306,9 +289,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); } } @@ -371,7 +354,6 @@ impl Resources { &mut self, component_id: ComponentId, components: &Components, - f: impl FnOnce() -> ArchetypeComponentId, ) -> &mut ResourceData { self.resources.get_or_insert_with(component_id, || { let component_info = components.get_info(component_id).unwrap(); @@ -394,8 +376,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()), - id: f(), + type_name: component_info.name(), #[cfg(feature = "std")] origin_thread_id: None, changed_by: MaybeLocation::caller().map(UnsafeCell::new), @@ -403,9 +384,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 69305e8a14..bb28f967af 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -1,15 +1,13 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, - entity::Entity, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, + entity::{Entity, EntityRow}, storage::{Column, TableRow}, }; use alloc::{boxed::Box, vec::Vec}; use bevy_ptr::{OwningPtr, Ptr}; use core::{cell::UnsafeCell, hash::Hash, marker::PhantomData, panic::Location}; -use nonmax::NonMaxUsize; - -type EntityIndex = u32; +use nonmax::{NonMaxU32, NonMaxUsize}; #[derive(Debug)] pub(crate) struct SparseArray { @@ -121,10 +119,10 @@ pub struct ComponentSparseSet { // stored for entities that are alive. The generation is not required, but is stored // in debug builds to validate that access is correct. #[cfg(not(debug_assertions))] - entities: Vec, + entities: Vec, #[cfg(debug_assertions)] entities: Vec, - sparse: SparseArray, + sparse: SparseArray, } impl ComponentSparseSet { @@ -170,20 +168,25 @@ impl ComponentSparseSet { change_tick: Tick, caller: MaybeLocation, ) { - if let Some(&dense_index) = self.sparse.get(entity.index()) { + if let Some(&dense_index) = self.sparse.get(entity.row()) { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); self.dense.replace(dense_index, value, change_tick, caller); } else { let dense_index = self.dense.len(); self.dense .push(value, ComponentTicks::new(change_tick), caller); - self.sparse - .insert(entity.index(), TableRow::from_usize(dense_index)); + + // SAFETY: This entity row does not exist here yet, so there are no duplicates, + // and the entity index can not be the max, so the length must not be max either. + // To do so would have caused a panic in the entity alloxator. + let table_row = unsafe { TableRow::new(NonMaxU32::new_unchecked(dense_index as u32)) }; + + self.sparse.insert(entity.row(), table_row); #[cfg(debug_assertions)] assert_eq!(self.entities.len(), dense_index); #[cfg(not(debug_assertions))] - self.entities.push(entity.index()); + self.entities.push(entity.row()); #[cfg(debug_assertions)] self.entities.push(entity); } @@ -194,16 +197,16 @@ impl ComponentSparseSet { pub fn contains(&self, entity: Entity) -> bool { #[cfg(debug_assertions)] { - if let Some(&dense_index) = self.sparse.get(entity.index()) { + if let Some(&dense_index) = self.sparse.get(entity.row()) { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); true } else { false } } #[cfg(not(debug_assertions))] - self.sparse.contains(entity.index()) + self.sparse.contains(entity.row()) } /// Returns a reference to the entity's component value. @@ -211,9 +214,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get(&self, entity: Entity) -> Option> { - self.sparse.get(entity.index()).map(|&dense_index| { + self.sparse.get(entity.row()).map(|&dense_index| { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { self.dense.get_data_unchecked(dense_index) } }) @@ -231,9 +234,9 @@ impl ComponentSparseSet { TickCells<'_>, MaybeLocation<&UnsafeCell<&'static Location<'static>>>, )> { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(( @@ -252,9 +255,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_added_tick(&self, entity: Entity) -> Option<&UnsafeCell> { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_added_tick_unchecked(dense_index)) } } @@ -264,9 +267,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_changed_tick(&self, entity: Entity) -> Option<&UnsafeCell> { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_changed_tick_unchecked(dense_index)) } } @@ -276,9 +279,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_ticks(&self, entity: Entity) -> Option { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_ticks_unchecked(dense_index)) } } @@ -292,9 +295,9 @@ impl ComponentSparseSet { entity: Entity, ) -> MaybeLocation>>> { MaybeLocation::new_with_flattened(|| { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_changed_by_unchecked(dense_index)) } }) @@ -311,19 +314,19 @@ impl ComponentSparseSet { /// it exists). #[must_use = "The returned pointer must be used to drop the removed component."] pub(crate) fn remove_and_forget(&mut self, entity: Entity) -> Option> { - self.sparse.remove(entity.index()).map(|dense_index| { + self.sparse.remove(entity.row()).map(|dense_index| { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); - self.entities.swap_remove(dense_index.as_usize()); - let is_last = dense_index.as_usize() == self.dense.len() - 1; + assert_eq!(entity, self.entities[dense_index.index()]); + self.entities.swap_remove(dense_index.index()); + let is_last = dense_index.index() == self.dense.len() - 1; // SAFETY: dense_index was just removed from `sparse`, which ensures that it is valid let (value, _, _) = unsafe { self.dense.swap_remove_and_forget_unchecked(dense_index) }; if !is_last { - let swapped_entity = self.entities[dense_index.as_usize()]; + let swapped_entity = self.entities[dense_index.index()]; #[cfg(not(debug_assertions))] let index = swapped_entity; #[cfg(debug_assertions)] - let index = swapped_entity.index(); + let index = swapped_entity.row(); *self.sparse.get_mut(index).unwrap() = dense_index; } value @@ -334,21 +337,21 @@ impl ComponentSparseSet { /// /// Returns `true` if `entity` had a component value in the sparse set. pub(crate) fn remove(&mut self, entity: Entity) -> bool { - if let Some(dense_index) = self.sparse.remove(entity.index()) { + if let Some(dense_index) = self.sparse.remove(entity.row()) { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); - self.entities.swap_remove(dense_index.as_usize()); - let is_last = dense_index.as_usize() == self.dense.len() - 1; + assert_eq!(entity, self.entities[dense_index.index()]); + self.entities.swap_remove(dense_index.index()); + let is_last = dense_index.index() == self.dense.len() - 1; // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { self.dense.swap_remove_unchecked(dense_index); } if !is_last { - let swapped_entity = self.entities[dense_index.as_usize()]; + let swapped_entity = self.entities[dense_index.index()]; #[cfg(not(debug_assertions))] let index = swapped_entity; #[cfg(debug_assertions)] - let index = swapped_entity.index(); + let index = swapped_entity.row(); *self.sparse.get_mut(index).unwrap() = dense_index; } true @@ -357,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); } } @@ -647,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 522df222c6..acf531d9b9 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -49,13 +49,13 @@ impl ThinColumn { row: TableRow, ) { self.data - .swap_remove_and_drop_unchecked_nonoverlapping(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked_nonoverlapping(row.index(), last_element_index); self.added_ticks - .swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); + .swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); self.changed_ticks - .swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); + .swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); self.changed_by.as_mut().map(|changed_by| { - changed_by.swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); + changed_by.swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); }); } @@ -71,13 +71,13 @@ impl ThinColumn { row: TableRow, ) { self.data - .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked(row.index(), last_element_index); self.added_ticks - .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked(row.index(), last_element_index); self.changed_ticks - .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked(row.index(), last_element_index); self.changed_by.as_mut().map(|changed_by| { - changed_by.swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + changed_by.swap_remove_and_drop_unchecked(row.index(), last_element_index); }); } @@ -94,14 +94,14 @@ impl ThinColumn { ) { let _ = self .data - .swap_remove_unchecked(row.as_usize(), last_element_index); + .swap_remove_unchecked(row.index(), last_element_index); self.added_ticks - .swap_remove_unchecked(row.as_usize(), last_element_index); + .swap_remove_unchecked(row.index(), last_element_index); self.changed_ticks - .swap_remove_unchecked(row.as_usize(), last_element_index); + .swap_remove_unchecked(row.index(), last_element_index); self.changed_by .as_mut() - .map(|changed_by| changed_by.swap_remove_unchecked(row.as_usize(), last_element_index)); + .map(|changed_by| changed_by.swap_remove_unchecked(row.index(), last_element_index)); } /// Call [`realloc`](std::alloc::realloc) to expand / shrink the memory allocation for this [`ThinColumn`] @@ -148,15 +148,12 @@ impl ThinColumn { tick: Tick, caller: MaybeLocation, ) { - self.data.initialize_unchecked(row.as_usize(), data); - *self.added_ticks.get_unchecked_mut(row.as_usize()).get_mut() = tick; - *self - .changed_ticks - .get_unchecked_mut(row.as_usize()) - .get_mut() = tick; + self.data.initialize_unchecked(row.index(), data); + *self.added_ticks.get_unchecked_mut(row.index()).get_mut() = tick; + *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = tick; self.changed_by .as_mut() - .map(|changed_by| changed_by.get_unchecked_mut(row.as_usize()).get_mut()) + .map(|changed_by| changed_by.get_unchecked_mut(row.index()).get_mut()) .assign(caller); } @@ -173,14 +170,11 @@ impl ThinColumn { change_tick: Tick, caller: MaybeLocation, ) { - self.data.replace_unchecked(row.as_usize(), data); - *self - .changed_ticks - .get_unchecked_mut(row.as_usize()) - .get_mut() = change_tick; + self.data.replace_unchecked(row.index(), data); + *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = change_tick; self.changed_by .as_mut() - .map(|changed_by| changed_by.get_unchecked_mut(row.as_usize()).get_mut()) + .map(|changed_by| changed_by.get_unchecked_mut(row.index()).get_mut()) .assign(caller); } @@ -206,25 +200,25 @@ impl ThinColumn { // Init the data let src_val = other .data - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); - self.data.initialize_unchecked(dst_row.as_usize(), src_val); + .swap_remove_unchecked(src_row.index(), other_last_element_index); + self.data.initialize_unchecked(dst_row.index(), src_val); // Init added_ticks let added_tick = other .added_ticks - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); + .swap_remove_unchecked(src_row.index(), other_last_element_index); self.added_ticks - .initialize_unchecked(dst_row.as_usize(), added_tick); + .initialize_unchecked(dst_row.index(), added_tick); // Init changed_ticks let changed_tick = other .changed_ticks - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); + .swap_remove_unchecked(src_row.index(), other_last_element_index); self.changed_ticks - .initialize_unchecked(dst_row.as_usize(), changed_tick); + .initialize_unchecked(dst_row.index(), changed_tick); self.changed_by.as_mut().zip(other.changed_by.as_mut()).map( |(self_changed_by, other_changed_by)| { let changed_by = other_changed_by - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); - self_changed_by.initialize_unchecked(dst_row.as_usize(), changed_by); + .swap_remove_unchecked(src_row.index(), other_last_element_index); + self_changed_by.initialize_unchecked(dst_row.index(), changed_by); }, ); } @@ -234,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); } } @@ -384,15 +378,12 @@ impl Column { change_tick: Tick, caller: MaybeLocation, ) { - debug_assert!(row.as_usize() < self.len()); - self.data.replace_unchecked(row.as_usize(), data); - *self - .changed_ticks - .get_unchecked_mut(row.as_usize()) - .get_mut() = change_tick; + debug_assert!(row.index() < self.len()); + self.data.replace_unchecked(row.index(), data); + *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = change_tick; self.changed_by .as_mut() - .map(|changed_by| changed_by.get_unchecked_mut(row.as_usize()).get_mut()) + .map(|changed_by| changed_by.get_unchecked_mut(row.index()).get_mut()) .assign(caller); } @@ -419,12 +410,12 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: TableRow) { - self.data.swap_remove_and_drop_unchecked(row.as_usize()); - self.added_ticks.swap_remove(row.as_usize()); - self.changed_ticks.swap_remove(row.as_usize()); + self.data.swap_remove_and_drop_unchecked(row.index()); + self.added_ticks.swap_remove(row.index()); + self.changed_ticks.swap_remove(row.index()); self.changed_by .as_mut() - .map(|changed_by| changed_by.swap_remove(row.as_usize())); + .map(|changed_by| changed_by.swap_remove(row.index())); } /// Removes an element from the [`Column`] and returns it and its change detection ticks. @@ -444,13 +435,13 @@ impl Column { &mut self, row: TableRow, ) -> (OwningPtr<'_>, ComponentTicks, MaybeLocation) { - let data = self.data.swap_remove_and_forget_unchecked(row.as_usize()); - let added = self.added_ticks.swap_remove(row.as_usize()).into_inner(); - let changed = self.changed_ticks.swap_remove(row.as_usize()).into_inner(); + let data = self.data.swap_remove_and_forget_unchecked(row.index()); + let added = self.added_ticks.swap_remove(row.index()).into_inner(); + let changed = self.changed_ticks.swap_remove(row.index()).into_inner(); let caller = self .changed_by .as_mut() - .map(|changed_by| changed_by.swap_remove(row.as_usize()).into_inner()); + .map(|changed_by| changed_by.swap_remove(row.index()).into_inner()); (data, ComponentTicks { added, changed }, caller) } @@ -520,15 +511,15 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get(&self, row: TableRow) -> Option<(Ptr<'_>, TickCells<'_>)> { - (row.as_usize() < self.data.len()) + (row.index() < self.data.len()) // SAFETY: The row is length checked before fetching the pointer. This is being // accessed through a read-only reference to the column. .then(|| unsafe { ( - self.data.get_unchecked(row.as_usize()), + self.data.get_unchecked(row.index()), TickCells { - added: self.added_ticks.get_unchecked(row.as_usize()), - changed: self.changed_ticks.get_unchecked(row.as_usize()), + added: self.added_ticks.get_unchecked(row.index()), + changed: self.changed_ticks.get_unchecked(row.index()), }, ) }) @@ -539,10 +530,10 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_data(&self, row: TableRow) -> Option> { - (row.as_usize() < self.data.len()).then(|| { + (row.index() < self.data.len()).then(|| { // SAFETY: The row is length checked before fetching the pointer. This is being // accessed through a read-only reference to the column. - unsafe { self.data.get_unchecked(row.as_usize()) } + unsafe { self.data.get_unchecked(row.index()) } }) } @@ -554,8 +545,8 @@ impl Column { /// - no other mutable reference to the data of the same row can exist at the same time #[inline] pub unsafe fn get_data_unchecked(&self, row: TableRow) -> Ptr<'_> { - debug_assert!(row.as_usize() < self.data.len()); - self.data.get_unchecked(row.as_usize()) + debug_assert!(row.index() < self.data.len()); + self.data.get_unchecked(row.index()) } /// Fetches a mutable reference to the data at `row`. @@ -563,10 +554,10 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_data_mut(&mut self, row: TableRow) -> Option> { - (row.as_usize() < self.data.len()).then(|| { + (row.index() < self.data.len()).then(|| { // SAFETY: The row is length checked before fetching the pointer. This is being // accessed through an exclusive reference to the column. - unsafe { self.data.get_unchecked_mut(row.as_usize()) } + unsafe { self.data.get_unchecked_mut(row.index()) } }) } @@ -579,7 +570,7 @@ impl Column { /// adhere to the safety invariants of [`UnsafeCell`]. #[inline] pub fn get_added_tick(&self, row: TableRow) -> Option<&UnsafeCell> { - self.added_ticks.get(row.as_usize()) + self.added_ticks.get(row.index()) } /// Fetches the "changed" change detection tick for the value at `row`. @@ -591,7 +582,7 @@ impl Column { /// adhere to the safety invariants of [`UnsafeCell`]. #[inline] pub fn get_changed_tick(&self, row: TableRow) -> Option<&UnsafeCell> { - self.changed_ticks.get(row.as_usize()) + self.changed_ticks.get(row.index()) } /// Fetches the change detection ticks for the value at `row`. @@ -599,7 +590,7 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_ticks(&self, row: TableRow) -> Option { - if row.as_usize() < self.data.len() { + if row.index() < self.data.len() { // SAFETY: The size of the column has already been checked. Some(unsafe { self.get_ticks_unchecked(row) }) } else { @@ -614,8 +605,8 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_added_tick_unchecked(&self, row: TableRow) -> &UnsafeCell { - debug_assert!(row.as_usize() < self.added_ticks.len()); - self.added_ticks.get_unchecked(row.as_usize()) + debug_assert!(row.index() < self.added_ticks.len()); + self.added_ticks.get_unchecked(row.index()) } /// Fetches the "changed" change detection tick for the value at `row`. Unlike [`Column::get_changed_tick`] @@ -625,8 +616,8 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_changed_tick_unchecked(&self, row: TableRow) -> &UnsafeCell { - debug_assert!(row.as_usize() < self.changed_ticks.len()); - self.changed_ticks.get_unchecked(row.as_usize()) + debug_assert!(row.index() < self.changed_ticks.len()); + self.changed_ticks.get_unchecked(row.index()) } /// Fetches the change detection ticks for the value at `row`. Unlike [`Column::get_ticks`] @@ -636,11 +627,11 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_ticks_unchecked(&self, row: TableRow) -> ComponentTicks { - debug_assert!(row.as_usize() < self.added_ticks.len()); - debug_assert!(row.as_usize() < self.changed_ticks.len()); + debug_assert!(row.index() < self.added_ticks.len()); + debug_assert!(row.index() < self.changed_ticks.len()); ComponentTicks { - added: self.added_ticks.get_unchecked(row.as_usize()).read(), - changed: self.changed_ticks.get_unchecked(row.as_usize()).read(), + added: self.added_ticks.get_unchecked(row.index()).read(), + changed: self.changed_ticks.get_unchecked(row.index()).read(), } } @@ -655,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); } } @@ -678,7 +669,7 @@ impl Column { ) -> MaybeLocation>>> { self.changed_by .as_ref() - .map(|changed_by| changed_by.get(row.as_usize())) + .map(|changed_by| changed_by.get(row.index())) } /// Fetches the calling location that last changed the value at `row`. @@ -693,8 +684,8 @@ impl Column { row: TableRow, ) -> MaybeLocation<&UnsafeCell<&'static Location<'static>>> { self.changed_by.as_ref().map(|changed_by| { - debug_assert!(row.as_usize() < changed_by.len()); - changed_by.get_unchecked(row.as_usize()) + debug_assert!(row.index() < changed_by.len()); + changed_by.get_unchecked(row.index()) }) } diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index fd8eb410e4..548ba82103 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}, @@ -16,6 +16,7 @@ use core::{ ops::{Index, IndexMut}, panic::Location, }; +use nonmax::NonMaxU32; mod column; /// An opaque unique ID for a [`Table`] within a [`World`]. @@ -35,8 +36,6 @@ mod column; pub struct TableId(u32); impl TableId { - pub(crate) const INVALID: TableId = TableId(u32::MAX); - /// Creates a new [`TableId`]. /// /// `index` *must* be retrieved from calling [`TableId::as_u32`] on a `TableId` you got @@ -100,39 +99,27 @@ impl TableId { /// [`Archetype::entity_table_row`]: crate::archetype::Archetype::entity_table_row /// [`Archetype::table_id`]: crate::archetype::Archetype::table_id #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TableRow(u32); +#[repr(transparent)] +pub struct TableRow(NonMaxU32); impl TableRow { - pub(crate) const INVALID: TableRow = TableRow(u32::MAX); - - /// Creates a `TableRow`. + /// Creates a [`TableRow`]. #[inline] - pub const fn from_u32(index: u32) -> Self { + pub const fn new(index: NonMaxU32) -> Self { Self(index) } - /// Creates a `TableRow` from a [`usize`] index. - /// - /// # Panics - /// - /// Will panic in debug mode if the provided value does not fit within a [`u32`]. - #[inline] - pub const fn from_usize(index: usize) -> Self { - debug_assert!(index as u32 as usize == index); - Self(index as u32) - } - /// Gets the index of the row as a [`usize`]. #[inline] - pub const fn as_usize(self) -> usize { + pub const fn index(self) -> usize { // usize is at least u32 in Bevy - self.0 as usize + self.0.get() as usize } /// Gets the index of the row as a [`usize`]. #[inline] - pub const fn as_u32(self) -> u32 { - self.0 + pub const fn index_u32(self) -> u32 { + self.0.get() } } @@ -225,9 +212,9 @@ impl Table { /// # Safety /// `row` must be in-bounds (`row.as_usize()` < `self.len()`) pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: TableRow) -> Option { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - if row.as_usize() != last_element_index { + if row.index_u32() != last_element_index { // Instead of checking this condition on every `swap_remove` call, we // check it here and use `swap_remove_nonoverlapping`. for col in self.columns.values_mut() { @@ -237,22 +224,26 @@ impl Table { // - `row` != `last_element_index` // - the `len` is kept within `self.entities`, it will update accordingly. unsafe { - col.swap_remove_and_drop_unchecked_nonoverlapping(last_element_index, row); + col.swap_remove_and_drop_unchecked_nonoverlapping( + last_element_index as usize, + row, + ); }; } } else { // If `row.as_usize()` == `last_element_index` than there's no point in removing the component // at `row`, but we still need to drop it. for col in self.columns.values_mut() { - col.drop_last_component(last_element_index); + col.drop_last_component(last_element_index as usize); } } - let is_last = row.as_usize() == last_element_index; - self.entities.swap_remove(row.as_usize()); + let is_last = row.index_u32() == last_element_index; + self.entities.swap_remove(row.index()); if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } } } @@ -269,16 +260,21 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - let is_last = row.as_usize() == last_element_index; - let new_row = new_table.allocate(self.entities.swap_remove(row.as_usize())); + let is_last = row.index_u32() == last_element_index; + let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { if let Some(new_column) = new_table.get_column_mut(*component_id) { - new_column.initialize_from_unchecked(column, last_element_index, row, new_row); + new_column.initialize_from_unchecked( + column, + last_element_index as usize, + row, + new_row, + ); } else { // It's the caller's responsibility to drop these cases. - column.swap_remove_and_forget_unchecked(last_element_index, row); + column.swap_remove_and_forget_unchecked(last_element_index as usize, row); } } TableMoveResult { @@ -286,7 +282,8 @@ impl Table { swapped_entity: if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } }, } } @@ -302,15 +299,20 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - let is_last = row.as_usize() == last_element_index; - let new_row = new_table.allocate(self.entities.swap_remove(row.as_usize())); + let is_last = row.index_u32() == last_element_index; + let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { if let Some(new_column) = new_table.get_column_mut(*component_id) { - new_column.initialize_from_unchecked(column, last_element_index, row, new_row); + new_column.initialize_from_unchecked( + column, + last_element_index as usize, + row, + new_row, + ); } else { - column.swap_remove_and_drop_unchecked(last_element_index, row); + column.swap_remove_and_drop_unchecked(last_element_index as usize, row); } } TableMoveResult { @@ -318,7 +320,8 @@ impl Table { swapped_entity: if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } }, } } @@ -335,22 +338,23 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - let is_last = row.as_usize() == last_element_index; - let new_row = new_table.allocate(self.entities.swap_remove(row.as_usize())); + let is_last = row.index_u32() == last_element_index; + let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { new_table .get_column_mut(*component_id) .debug_checked_unwrap() - .initialize_from_unchecked(column, last_element_index, row, new_row); + .initialize_from_unchecked(column, last_element_index as usize, row, new_row); } TableMoveResult { new_row, swapped_entity: if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } }, } } @@ -365,7 +369,7 @@ impl Table { component_id: ComponentId, ) -> Option<&[UnsafeCell]> { self.get_column(component_id) - .map(|col| col.get_data_slice(self.entity_count())) + .map(|col| col.get_data_slice(self.entity_count() as usize)) } /// Get the added ticks of the column matching `component_id` as a slice. @@ -375,7 +379,7 @@ impl Table { ) -> Option<&[UnsafeCell]> { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the ticks array - .map(|col| unsafe { col.get_added_ticks_slice(self.entity_count()) }) + .map(|col| unsafe { col.get_added_ticks_slice(self.entity_count() as usize) }) } /// Get the changed ticks of the column matching `component_id` as a slice. @@ -385,7 +389,7 @@ impl Table { ) -> Option<&[UnsafeCell]> { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the ticks array - .map(|col| unsafe { col.get_changed_ticks_slice(self.entity_count()) }) + .map(|col| unsafe { col.get_changed_ticks_slice(self.entity_count() as usize) }) } /// Fetches the calling locations that last changed the each component @@ -396,7 +400,7 @@ impl Table { MaybeLocation::new_with_flattened(|| { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the locations array - .map(|col| unsafe { col.get_changed_by_slice(self.entity_count()) }) + .map(|col| unsafe { col.get_changed_by_slice(self.entity_count() as usize) }) }) } @@ -406,12 +410,12 @@ impl Table { component_id: ComponentId, row: TableRow, ) -> Option<&UnsafeCell> { - (row.as_usize() < self.entity_count()).then_some( + (row.index_u32() < self.entity_count()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? .changed_ticks - .get_unchecked(row.as_usize()) + .get_unchecked(row.index()) }, ) } @@ -422,12 +426,12 @@ impl Table { component_id: ComponentId, row: TableRow, ) -> Option<&UnsafeCell> { - (row.as_usize() < self.entity_count()).then_some( + (row.index_u32() < self.entity_count()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? .added_ticks - .get_unchecked(row.as_usize()) + .get_unchecked(row.index()) }, ) } @@ -439,13 +443,13 @@ impl Table { row: TableRow, ) -> MaybeLocation>>> { MaybeLocation::new_with_flattened(|| { - (row.as_usize() < self.entity_count()).then_some( + (row.index_u32() < self.entity_count()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? .changed_by .as_ref() - .map(|changed_by| changed_by.get_unchecked(row.as_usize())) + .map(|changed_by| changed_by.get_unchecked(row.index())) }, ) }) @@ -461,8 +465,8 @@ impl Table { row: TableRow, ) -> Option { self.get_column(component_id).map(|col| ComponentTicks { - added: col.added_ticks.get_unchecked(row.as_usize()).read(), - changed: col.changed_ticks.get_unchecked(row.as_usize()).read(), + added: col.added_ticks.get_unchecked(row.index()).read(), + changed: col.changed_ticks.get_unchecked(row.index()).read(), }) } @@ -499,7 +503,7 @@ impl Table { /// Reserves `additional` elements worth of capacity within the table. pub(crate) fn reserve(&mut self, additional: usize) { - if self.capacity() - self.entity_count() < additional { + if (self.capacity() - self.entity_count() as usize) < additional { let column_cap = self.capacity(); self.entities.reserve(additional); @@ -563,10 +567,15 @@ impl Table { /// Allocates space for a new entity /// /// # Safety - /// the allocated row must be written to immediately with valid values in each column + /// + /// The allocated row must be written to immediately with valid values in each column pub(crate) unsafe fn allocate(&mut self, entity: Entity) -> TableRow { self.reserve(1); let len = self.entity_count(); + // SAFETY: No entity row may be in more than one table row at once, so there are no duplicates, + // and there can not be an entity row of u32::MAX. Therefore, this can not be max either. + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(len)) }; + let len = len as usize; self.entities.push(entity); for col in self.columns.values_mut() { col.added_ticks @@ -580,13 +589,16 @@ impl Table { changed_by.initialize_unchecked(len, UnsafeCell::new(caller)); }); } - TableRow::from_usize(len) + + row } /// Gets the number of entities currently being stored in the table. #[inline] - pub fn entity_count(&self) -> usize { - self.entities.len() + pub fn entity_count(&self) -> u32 { + // No entity may have more than one table row, so there are no duplicates, + // and there may only ever be u32::MAX entities, so the length never exceeds u32's capacity. + self.entities.len() as u32 } /// Get the drop function for some component that is stored in this table. @@ -617,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) { - let len = self.entity_count(); + 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) }; } } @@ -632,7 +644,7 @@ impl Table { /// Clears all of the stored components in the [`Table`]. pub(crate) fn clear(&mut self) { - let len = self.entity_count(); + let len = self.entity_count() as usize; // We must clear the entities first, because in the drop function causes a panic, it will result in a double free of the columns. self.entities.clear(); for column in self.columns.values_mut() { @@ -660,7 +672,7 @@ impl Table { self.get_column_mut(component_id) .debug_checked_unwrap() .data - .get_unchecked_mut(row.as_usize()) + .get_unchecked_mut(row.index()) .promote() } @@ -674,7 +686,7 @@ impl Table { row: TableRow, ) -> Option> { self.get_column(component_id) - .map(|col| col.data.get_unchecked(row.as_usize())) + .map(|col| col.data.get_unchecked(row.index())) } } @@ -781,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); } } } @@ -806,7 +818,7 @@ impl IndexMut for Tables { impl Drop for Table { fn drop(&mut self) { - let len = self.entity_count(); + let len = self.entity_count() as usize; let cap = self.capacity(); self.entities.clear(); for col in self.columns.values_mut() { diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 5953a43d70..6b1334862e 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -1,6 +1,7 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; -use super::{IntoSystem, ReadOnlySystem, System, SystemParamValidationError}; +use super::{IntoSystem, ReadOnlySystem, RunSystemError, System, SystemParamValidationError}; use crate::{ schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, @@ -13,7 +14,7 @@ use crate::{ /// /// ``` /// # use bevy_ecs::prelude::*; -/// use bevy_ecs::system::{Adapt, AdapterSystem}; +/// use bevy_ecs::system::{Adapt, AdapterSystem, RunSystemError}; /// /// // A system adapter that inverts the result of a system. /// // NOTE: Instead of manually implementing this, you can just use `bevy_ecs::schedule::common_conditions::not`. @@ -33,15 +34,16 @@ use crate::{ /// fn adapt( /// &mut self, /// input: ::Inner<'_>, -/// run_system: impl FnOnce(SystemIn<'_, S>) -> S::Out, -/// ) -> Self::Out { -/// !run_system(input) +/// run_system: impl FnOnce(SystemIn<'_, S>) -> Result, +/// ) -> Result { +/// let result = run_system(input)?; +/// Ok(!result) /// } /// } /// # let mut world = World::new(); /// # let mut system = NotSystem::new(NotMarker, IntoSystem::into_system(|| false), "".into()); /// # system.initialize(&mut world); -/// # assert!(system.run((), &mut world)); +/// # assert!(system.run((), &mut world).unwrap()); /// ``` #[diagnostic::on_unimplemented( message = "`{Self}` can not adapt a system of type `{S}`", @@ -58,8 +60,8 @@ pub trait Adapt: Send + Sync + 'static { fn adapt( &mut self, input: ::Inner<'_>, - run_system: impl FnOnce(SystemIn<'_, S>) -> S::Out, - ) -> Self::Out; + run_system: impl FnOnce(SystemIn<'_, S>) -> Result, + ) -> Result; } /// An [`IntoSystem`] creating an instance of [`AdapterSystem`]. @@ -101,7 +103,7 @@ where pub struct AdapterSystem { func: Func, system: S, - name: Cow<'static, str>, + name: DebugName, } impl AdapterSystem @@ -110,7 +112,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,37 +125,13 @@ 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(&self) -> &crate::query::Access { - self.system.component_access() - } - - fn component_access_set( - &self, - ) -> &crate::query::FilteredAccessSet { - self.system.component_access_set() - } - #[inline] - fn archetype_component_access( - &self, - ) -> &crate::query::Access { - self.system.archetype_component_access() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + fn flags(&self) -> super::SystemStateFlags { + self.system.flags() } #[inline] @@ -161,13 +139,19 @@ where &mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell, - ) -> Self::Out { + ) -> Result { // SAFETY: `system.run_unsafe` has the same invariants as `self.run_unsafe`. self.func.adapt(input, |input| unsafe { self.system.run_unsafe(input, world) }) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut crate::prelude::World) { self.system.apply_deferred(world); @@ -187,17 +171,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) } - #[inline] - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.system.update_archetype_component_access(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 { @@ -232,8 +214,8 @@ where fn adapt( &mut self, input: ::Inner<'_>, - run_system: impl FnOnce(SystemIn<'_, S>) -> S::Out, - ) -> Out { - self(run_system(input)) + run_system: impl FnOnce(SystemIn<'_, S>) -> Result, + ) -> Result { + run_system(input).map(self) } } diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 441d4d42fc..4f4366b5e2 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -1,5 +1,5 @@ use alloc::{boxed::Box, vec::Vec}; -use bevy_utils::synccell::SyncCell; +use bevy_platform::cell::SyncCell; use variadics_please::all_tuples; use crate::{ @@ -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,85 +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); - system_meta - .archetype_component_access - .extend(&$meta.archetype_component_access); - )* - #[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 and ArchetypeComponentId 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); - system_meta - .archetype_component_access - .extend(&meta.archetype_component_access); - } - 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)) })) } } @@ -524,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) } } @@ -555,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) } } @@ -591,44 +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://bevyengine.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - meta.archetype_component_access.read_all_resources(); - } else { - for component_id in access.resource_reads_and_writes() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - - let archetype_component_id = world.initialize_resource_internal(component_id).id(); - meta.archetype_component_access - .add_resource_read(archetype_component_id); - } - } - - access + builder.build() } } @@ -655,59 +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://bevyengine.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - meta.archetype_component_access.read_all_resources(); - } else { - for component_id in access.resource_reads() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - - let archetype_component_id = world.initialize_resource_internal(component_id).id(); - meta.archetype_component_access - .add_resource_read(archetype_component_id); - } - } - - if access.has_write_all_resources() { - meta.component_access_set - .add_unfiltered_write_all_resources(); - meta.archetype_component_access.write_all_resources(); - } else { - for component_id in access.resource_writes() { - meta.component_access_set - .add_unfiltered_resource_write(component_id); - - let archetype_component_id = world.initialize_resource_internal(component_id).id(); - meta.archetype_component_access - .add_resource_write(archetype_component_id); - } - } - - access + builder.build() } } @@ -719,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) } } @@ -735,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) } } @@ -749,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) } } @@ -758,6 +622,7 @@ unsafe impl> SystemParamBuilder mod tests { use crate::{ entity::Entities, + error::Result, prelude::{Component, Query}, reflect::ReflectResource, system::{Local, RunSystemOnce}, @@ -790,6 +655,10 @@ mod tests { query.iter().count() } + fn query_system_result(query: Query<()>) -> Result { + Ok(query.iter().count()) + } + fn multi_param_system(a: Local, b: Local) -> u64 { *a + *b + 1 } @@ -823,6 +692,44 @@ mod tests { assert_eq!(output, 1); } + #[test] + fn query_builder_result_fallible() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let system = (QueryParamBuilder::new(|query| { + query.with::(); + }),) + .build_state(&mut world) + .build_system(query_system_result); + + // The type annotation here is necessary since the system + // could also return `Result` + let output: usize = world.run_system_once(system).unwrap(); + assert_eq!(output, 1); + } + + #[test] + fn query_builder_result_infallible() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let system = (QueryParamBuilder::new(|query| { + query.with::(); + }),) + .build_state(&mut world) + .build_system(query_system_result); + + // The type annotation here is necessary since the system + // could also return `usize` + let output: Result = world.run_system_once(system).unwrap(); + assert_eq!(output.unwrap(), 1); + } + #[test] fn query_builder_state() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 9d11de9525..1fc69d1c46 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -1,17 +1,17 @@ -use alloc::{borrow::Cow, format, vec::Vec}; +use alloc::{format, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::World, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn, SystemParamValidationError}, world::unsafe_world_cell::UnsafeWorldCell, }; -use super::{IntoSystem, ReadOnlySystem, System}; +use super::{IntoSystem, ReadOnlySystem, RunSystemError, System}; /// Customizes the behavior of a [`CombinatorSystem`]. /// @@ -19,7 +19,7 @@ use super::{IntoSystem, ReadOnlySystem, System}; /// /// ``` /// use bevy_ecs::prelude::*; -/// use bevy_ecs::system::{CombinatorSystem, Combine}; +/// use bevy_ecs::system::{CombinatorSystem, Combine, RunSystemError}; /// /// // A system combinator that performs an exclusive-or (XOR) /// // operation on the output of two systems. @@ -38,10 +38,10 @@ use super::{IntoSystem, ReadOnlySystem, System}; /// /// fn combine( /// _input: Self::In, -/// a: impl FnOnce(A::In) -> A::Out, -/// b: impl FnOnce(B::In) -> B::Out, -/// ) -> Self::Out { -/// a(()) ^ b(()) +/// a: impl FnOnce(A::In) -> Result, +/// b: impl FnOnce(B::In) -> Result, +/// ) -> Result { +/// Ok(a(())? ^ b(())?) /// } /// } /// @@ -56,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; } /// # @@ -101,9 +101,9 @@ pub trait Combine { /// See the trait-level docs for [`Combine`] for an example implementation. fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> A::Out, - b: impl FnOnce(SystemIn<'_, B>) -> B::Out, - ) -> Self::Out; + a: impl FnOnce(SystemIn<'_, A>) -> Result, + b: impl FnOnce(SystemIn<'_, B>) -> Result, + ) -> Result; } /// A [`System`] defined by combining two other systems. @@ -113,23 +113,19 @@ pub struct CombinatorSystem { _marker: PhantomData Func>, a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, - archetype_component_access: Access, + 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(), - archetype_component_access: Access::new(), } } } @@ -143,39 +139,20 @@ 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(&self) -> &Access { - self.component_access_set.combined_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - fn archetype_component_access(&self) -> &Access { - &self.archetype_component_access - } - - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( &mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell, - ) -> Self::Out { + ) -> Result { Func::combine( input, // SAFETY: The world accesses for both underlying systems have been registered, @@ -183,14 +160,24 @@ where // If either system has `is_exclusive()`, then the combined system also has `is_exclusive`. // Since these closures are `!Send + !Sync + !'static`, they can never be called // in parallel, so their world accesses will not conflict with each other. - // Additionally, `update_archetype_component_access` has been called, - // which forwards to the implementations for `self.a` and `self.b`. |input| unsafe { self.a.run_unsafe(input, world) }, + // `Self::validate_param_unsafe` already validated the first system, + // but we still need to validate the second system once the first one runs. // SAFETY: See the comment above. - |input| unsafe { self.b.run_unsafe(input, world) }, + |input| unsafe { + self.b.validate_param_unsafe(world)?; + self.b.run_unsafe(input, world) + }, ) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.a.refresh_hotpatch(); + self.b.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.a.apply_deferred(world); @@ -208,32 +195,24 @@ where &mut self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { + // We only validate parameters for the first system, + // since it may make changes to the world that affect + // whether the second system has valid parameters. + // The second system will be validated in `Self::run_unsafe`. // SAFETY: Delegate to other `System` implementations. 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 update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.a.update_archetype_component_access(world); - self.b.update_archetype_component_access(world); - - self.archetype_component_access - .extend(self.a.archetype_component_access()); - self.archetype_component_access - .extend(self.b.archetype_component_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 { @@ -302,7 +281,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)) } } @@ -331,7 +310,7 @@ where /// // pipe the `parse_message_system`'s output into the `filter_system`s input /// let mut piped_system = IntoSystem::into_system(parse_message_system.pipe(filter_system)); /// piped_system.initialize(&mut world); -/// assert_eq!(piped_system.run((), &mut world), Some(42)); +/// assert_eq!(piped_system.run((), &mut world).unwrap(), Some(42)); /// } /// /// #[derive(Resource)] @@ -348,9 +327,7 @@ where pub struct PipeSystem { a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, - archetype_component_access: Access, + name: DebugName, } impl PipeSystem @@ -360,14 +337,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(), - archetype_component_access: Access::new(), - } + pub fn new(a: A, b: B, name: DebugName) -> Self { + Self { a, b, name } } } @@ -380,43 +351,34 @@ 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(&self) -> &Access { - self.component_access_set.combined_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - fn archetype_component_access(&self) -> &Access { - &self.archetype_component_access - } - - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( &mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell, - ) -> Self::Out { - let value = self.a.run_unsafe(input, world); + ) -> Result { + let value = self.a.run_unsafe(input, world)?; + // `Self::validate_param_unsafe` already validated the first system, + // but we still need to validate the second system once the first one runs. + self.b.validate_param_unsafe(world)?; self.b.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.a.refresh_hotpatch(); + self.b.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.a.apply_deferred(world); self.b.apply_deferred(world); @@ -427,51 +389,28 @@ where self.b.queue_deferred(world); } - /// This method uses "early out" logic: if the first system fails validation, - /// the second system is not validated. - /// - /// Because the system validation is performed upfront, this can lead to situations - /// where later systems pass validation, but fail at runtime due to changes made earlier - /// in the piped systems. - // TODO: ensure that systems are only validated just before they are run. - // Fixing this will require fundamentally rethinking how piped systems work: - // they're currently treated as a single system from the perspective of the scheduler. - // See https://github.com/bevyengine/bevy/issues/18796 unsafe fn validate_param_unsafe( &mut self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { + // We only validate parameters for the first system, + // since it may make changes to the world that affect + // whether the second system has valid parameters. + // The second system will be validated in `Self::run_unsafe`. // SAFETY: Delegate to the `System` implementation for `a`. - unsafe { self.a.validate_param_unsafe(world) }?; - - // SAFETY: Delegate to the `System` implementation for `b`. - unsafe { self.b.validate_param_unsafe(world) }?; - - Ok(()) + 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 update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.a.update_archetype_component_access(world); - self.b.update_archetype_component_access(world); - - self.archetype_component_access - .extend(self.a.archetype_component_access()); - self.archetype_component_access - .extend(self.b.archetype_component_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..83dad342a8 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, @@ -208,7 +208,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 +217,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,12 +229,19 @@ pub fn trigger_targets( } } -/// A [`Command`] that sends an arbitrary [`Event`]. +/// A [`Command`] that writes an arbitrary [`BufferedEvent`]. #[track_caller] -pub fn send_event(event: E) -> impl Command { +pub fn write_event(event: E) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { let mut events = world.resource_mut::>(); - events.send_with_caller(event, caller); + events.write_with_caller(event, caller); } } + +/// A [`Command`] that writes an arbitrary [`BufferedEvent`]. +#[track_caller] +#[deprecated(since = "0.17.0", note = "Use `write_event` instead.")] +pub fn send_event(event: E) -> impl Command { + write_event(event) +} diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 317ad8476a..6d977e808d 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}, @@ -143,12 +143,38 @@ pub unsafe fn insert_by_id( /// An [`EntityCommand`] that adds a component to an entity using /// the component's [`FromWorld`] implementation. +/// +/// `T::from_world` will only be invoked if the component will actually be inserted. +/// In other words, `T::from_world` will *not* be invoked if `mode` is [`InsertMode::Keep`] +/// and the entity already has the component. #[track_caller] pub fn insert_from_world(mode: InsertMode) -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { - let value = entity.world_scope(|world| T::from_world(world)); - entity.insert_with_caller(value, mode, caller, RelationshipHookMode::Run); + if !(mode == InsertMode::Keep && entity.contains::()) { + let value = entity.world_scope(|world| T::from_world(world)); + entity.insert_with_caller(value, mode, caller, RelationshipHookMode::Run); + } + } +} + +/// An [`EntityCommand`] that adds a component to an entity using +/// some function that returns the component. +/// +/// The function will only be invoked if the component will actually be inserted. +/// In other words, the function will *not* be invoked if `mode` is [`InsertMode::Keep`] +/// and the entity already has the component. +#[track_caller] +pub fn insert_with(component_fn: F, mode: InsertMode) -> impl EntityCommand +where + F: FnOnce() -> T + Send + 'static, +{ + let caller = MaybeLocation::caller(); + move |mut entity: EntityWorldMut| { + if !(mode == InsertMode::Keep && entity.contains::()) { + let value = component_fn(); + entity.insert_with_caller(value, mode, caller, RelationshipHookMode::Run); + } } } @@ -218,7 +244,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 +253,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 +269,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 621a9de77e..a43dea5627 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -16,11 +16,11 @@ use core::marker::PhantomData; use crate::{ self as bevy_ecs, bundle::{Bundle, InsertMode, NoBundleEffect}, - change_detection::Mut, + change_detection::{MaybeLocation, Mut}, component::{Component, ComponentId, Mutable}, - entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, - error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, - event::Event, + entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut}, + error::{warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, + event::{BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, resource::Resource, schedule::ScheduleLabel, @@ -88,8 +88,8 @@ use crate::{ /// A [`Command`] can return a [`Result`](crate::error::Result), /// which will be passed to an [error handler](crate::error) if the `Result` is an error. /// -/// The [default error handler](crate::error::default_error_handler) panics. -/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`. +/// The default error handler panics. It can be configured via +/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource. /// /// Alternatively, you can customize the error handler for a specific command /// by calling [`Commands::queue_handled`]. @@ -120,31 +120,26 @@ 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, ), } } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &bevy_ecs::archetype::Archetype, + fn init_access( + state: &Self::State, system_meta: &mut bevy_ecs::system::SystemMeta, + component_access_set: &mut bevy_ecs::query::FilteredAccessSet, + world: &mut World, ) { - // SAFETY: Caller guarantees the archetype is from the world used in `init_state` - unsafe { - <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::new_archetype( - &mut state.state, - archetype, - system_meta, - ); - }; + <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); } fn apply( @@ -173,12 +168,12 @@ const _: () = { #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &bevy_ecs::system::SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { <(Deferred, &Entities) as bevy_ecs::system::SystemParam>::validate_param( - &state.state, + &mut state.state, system_meta, world, ) @@ -317,12 +312,24 @@ impl<'w, 's> Commands<'w, 's> { /// - [`spawn`](Self::spawn) to spawn an entity with components. /// - [`spawn_batch`](Self::spawn_batch) to spawn many entities /// with the same combination of components. + #[track_caller] pub fn spawn_empty(&mut self) -> EntityCommands { let entity = self.entities.reserve_entity(); - EntityCommands { + let mut entity_commands = EntityCommands { entity, commands: self.reborrow(), - } + }; + let caller = MaybeLocation::caller(); + entity_commands.queue(move |entity: EntityWorldMut| { + let index = entity.id().index(); + let world = entity.into_world_mut(); + let tick = world.change_tick(); + // SAFETY: Entity has been flushed + unsafe { + world.entities_mut().mark_spawn_despawn(index, caller, tick); + } + }); + entity_commands } /// Spawns a new [`Entity`] with the given components @@ -369,9 +376,35 @@ impl<'w, 's> Commands<'w, 's> { /// with the same combination of components. #[track_caller] pub fn spawn(&mut self, bundle: T) -> EntityCommands { - let mut entity = self.spawn_empty(); - entity.insert(bundle); - entity + let entity = self.entities.reserve_entity(); + let mut entity_commands = EntityCommands { + entity, + commands: self.reborrow(), + }; + let caller = MaybeLocation::caller(); + + entity_commands.queue(move |mut entity: EntityWorldMut| { + // Store metadata about the spawn operation. + // This is the same as in `spawn_empty`, but merged into + // the same command for better performance. + let index = entity.id().index(); + entity.world_scope(|world| { + let tick = world.change_tick(); + // SAFETY: Entity has been flushed + unsafe { + world.entities_mut().mark_spawn_despawn(index, caller, tick); + } + }); + + entity.insert_with_caller( + bundle, + InsertMode::Replace, + caller, + crate::relationship::RelationshipHookMode::Run, + ); + }); + // entity_command::insert(bundle, InsertMode::Replace) + entity_commands } /// Returns the [`EntityCommands`] for the given [`Entity`]. @@ -443,7 +476,7 @@ impl<'w, 's> Commands<'w, 's> { /// // Return from the system successfully. /// Ok(()) /// } - /// # bevy_ecs::system::assert_is_system(example_system); + /// # bevy_ecs::system::assert_is_system::<(), (), _>(example_system); /// ``` /// /// # See also @@ -508,7 +541,7 @@ impl<'w, 's> Commands<'w, 's> { /// Pushes a generic [`Command`] to the command queue. /// /// If the [`Command`] returns a [`Result`], - /// it will be handled using the [default error handler](crate::error::default_error_handler). + /// it will be handled using the [default error handler](crate::error::DefaultErrorHandler). /// /// To use a custom error handler, see [`Commands::queue_handled`]. /// @@ -608,6 +641,11 @@ impl<'w, 's> Commands<'w, 's> { self.queue_internal(command.handle_error_with(error_handler)); } + /// Pushes a generic [`Command`] to the queue like [`Commands::queue_handled`], but instead silently ignores any errors. + pub fn queue_silenced + HandleError, T>(&mut self, command: C) { + self.queue_internal(command.ignore_error()); + } + fn queue_internal(&mut self, command: impl Command) { match &mut self.queue { InternalQueue::CommandQueue(queue) => { @@ -643,7 +681,7 @@ impl<'w, 's> Commands<'w, 's> { /// This command will fail if any of the given entities do not exist. /// /// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError), - /// which will be handled by the [default error handler](crate::error::default_error_handler). + /// which will be handled by the [default error handler](crate::error::DefaultErrorHandler). #[track_caller] pub fn insert_batch(&mut self, batch: I) where @@ -674,7 +712,7 @@ impl<'w, 's> Commands<'w, 's> { /// This command will fail if any of the given entities do not exist. /// /// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError), - /// which will be handled by the [default error handler](crate::error::default_error_handler). + /// which will be handled by the [default error handler](crate::error::DefaultErrorHandler). #[track_caller] pub fn insert_batch_if_new(&mut self, batch: I) where @@ -1045,7 +1083,7 @@ impl<'w, 's> Commands<'w, 's> { 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] @@ -1053,13 +1091,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)); @@ -1068,6 +1106,8 @@ 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 [`On`]. + /// /// **Calling [`observe`](EntityCommands::observe) on the returned /// [`EntityCommands`] will observe the observer itself, which you very /// likely do not want.** @@ -1075,6 +1115,8 @@ impl<'w, 's> Commands<'w, 's> { /// # Panics /// /// Panics if the given system is an exclusive system. + /// + /// [`On`]: crate::observer::On pub fn add_observer( &mut self, observer: impl IntoObserverSystem, @@ -1082,9 +1124,9 @@ impl<'w, 's> Commands<'w, 's> { self.spawn(Observer::new(observer)) } - /// Sends an arbitrary [`Event`]. + /// Writes an arbitrary [`BufferedEvent`]. /// - /// This is a convenience method for sending events + /// This is a convenience method for writing events /// without requiring an [`EventWriter`](crate::event::EventWriter). /// /// # Performance @@ -1095,11 +1137,29 @@ 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 { - self.queue(command::send_event(event)); + pub fn write_event(&mut self, event: E) -> &mut Self { + self.queue(command::write_event(event)); self } + /// Writes an arbitrary [`BufferedEvent`]. + /// + /// This is a convenience method for writing events + /// without requiring an [`EventWriter`](crate::event::EventWriter). + /// + /// # Performance + /// + /// Since this is a command, exclusive world access is used, which means that it will not profit from + /// system-level parallelism on supported platforms. + /// + /// If these events are performance-critical or very frequently sent, + /// consider using a typed [`EventWriter`](crate::event::EventWriter) instead. + #[track_caller] + #[deprecated(since = "0.17.0", note = "Use `Commands::write_event` instead.")] + pub fn send_event(&mut self, event: E) -> &mut Self { + self.write_event(event) + } + /// Runs the schedule corresponding to the given [`ScheduleLabel`]. /// /// Calls [`World::try_run_schedule`](World::try_run_schedule). @@ -1175,8 +1235,8 @@ impl<'w, 's> Commands<'w, 's> { /// An [`EntityCommand`] can return a [`Result`](crate::error::Result), /// which will be passed to an [error handler](crate::error) if the `Result` is an error. /// -/// The [default error handler](crate::error::default_error_handler) panics. -/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`. +/// The default error handler panics. It can be configured via +/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource. /// /// Alternatively, you can customize the error handler for a specific command /// by calling [`EntityCommands::queue_handled`]. @@ -1231,15 +1291,32 @@ impl<'a> EntityCommands<'a> { /// #[derive(Component)] /// struct Level(u32); /// + /// + /// #[derive(Component, Default)] + /// struct Mana { + /// max: u32, + /// current: u32, + /// } + /// /// fn level_up_system(mut commands: Commands, player: Res) { + /// // If a component already exists then modify it, otherwise insert a default value /// commands /// .entity(player.entity) /// .entry::() - /// // Modify the component if it exists. /// .and_modify(|mut lvl| lvl.0 += 1) - /// // Otherwise, insert a default value. /// .or_insert(Level(0)); + /// + /// // Add a default value if none exists, and then modify the existing or new value + /// commands + /// .entity(player.entity) + /// .entry::() + /// .or_default() + /// .and_modify(|mut mana| { + /// mana.max += 10; + /// mana.current = mana.max; + /// }); /// } + /// /// # bevy_ecs::system::assert_is_system(level_up_system); /// ``` pub fn entry(&mut self) -> EntityEntryCommands { @@ -1410,12 +1487,11 @@ impl<'a> EntityCommands<'a> { component_id: ComponentId, value: T, ) -> &mut Self { - self.queue_handled( + self.queue_silenced( // SAFETY: // - `ComponentId` safety is ensured by the caller. // - `T` safety is ensured by the caller. unsafe { entity_command::insert_by_id(component_id, value, InsertMode::Replace) }, - ignore, ) } @@ -1467,7 +1543,7 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue_handled(entity_command::insert(bundle, InsertMode::Replace), ignore) + self.queue_silenced(entity_command::insert(bundle, InsertMode::Replace)) } /// Adds a [`Bundle`] of components to the entity if the predicate returns true. @@ -1523,7 +1599,7 @@ impl<'a> EntityCommands<'a> { /// the resulting error will be ignored. #[track_caller] pub fn try_insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue_handled(entity_command::insert(bundle, InsertMode::Keep), ignore) + self.queue_silenced(entity_command::insert(bundle, InsertMode::Keep)) } /// Removes a [`Bundle`] of components from the entity. @@ -1668,7 +1744,7 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` pub fn try_remove(&mut self) -> &mut Self { - self.queue_handled(entity_command::remove::(), ignore) + self.queue_silenced(entity_command::remove::()) } /// Removes a [`Bundle`] of components from the entity, @@ -1762,13 +1838,13 @@ impl<'a> EntityCommands<'a> { /// /// For example, this will recursively despawn [`Children`](crate::hierarchy::Children). pub fn try_despawn(&mut self) { - self.queue_handled(entity_command::despawn(), ignore); + self.queue_silenced(entity_command::despawn()); } /// Pushes an [`EntityCommand`] to the queue, /// which will get executed for the current [`Entity`]. /// - /// The [default error handler](crate::error::default_error_handler) + /// The [default error handler](crate::error::DefaultErrorHandler) /// will be used to handle error cases. /// Every [`EntityCommand`] checks whether the entity exists at the time of execution /// and returns an error if it does not. @@ -1851,6 +1927,18 @@ impl<'a> EntityCommands<'a> { self } + /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. + /// + /// Unlike [`EntityCommands::queue_handled`], this will completely ignore any errors that occur. + pub fn queue_silenced + CommandWithEntity, T, M>( + &mut self, + command: C, + ) -> &mut Self { + self.commands + .queue_silenced(command.with_entity(self.entity)); + self + } + /// Removes all components except the given [`Bundle`] from the entity. /// /// # Example @@ -1903,16 +1991,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 { @@ -1922,8 +2010,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 /// @@ -1931,7 +2020,7 @@ impl<'a> EntityCommands<'a> { /// /// # Example /// - /// Configure through [`EntityClonerBuilder`] as follows: + /// Configure through [`EntityClonerBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; /// #[derive(Component, Clone)] @@ -1946,8 +2035,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::(); /// }); /// } @@ -1955,12 +2044,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. @@ -1969,7 +2103,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 /// @@ -1989,25 +2124,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 /// @@ -2027,18 +2159,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, @@ -2114,35 +2291,53 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// [Insert](EntityCommands::insert) the value returned from `default` into this entity, /// if `T` is not already present. + /// + /// `default` will only be invoked if the component will actually be inserted. #[track_caller] - pub fn or_insert_with(&mut self, default: impl Fn() -> T) -> &mut Self { - self.or_insert(default()) + pub fn or_insert_with(&mut self, default: F) -> &mut Self + where + F: FnOnce() -> T + Send + 'static, + { + self.entity_commands + .queue(entity_command::insert_with(default, InsertMode::Keep)); + self } /// [Insert](EntityCommands::insert) the value returned from `default` into this entity, /// if `T` is not already present. /// + /// `default` will only be invoked if the component will actually be inserted. + /// /// # Note /// /// If the entity does not exist when this command is executed, /// the resulting error will be ignored. #[track_caller] - pub fn or_try_insert_with(&mut self, default: impl Fn() -> T) -> &mut Self { - self.or_try_insert(default()) + pub fn or_try_insert_with(&mut self, default: F) -> &mut Self + where + F: FnOnce() -> T + Send + 'static, + { + self.entity_commands + .queue_silenced(entity_command::insert_with(default, InsertMode::Keep)); + self } /// [Insert](EntityCommands::insert) `T::default` into this entity, /// if `T` is not already present. + /// + /// `T::default` will only be invoked if the component will actually be inserted. #[track_caller] pub fn or_default(&mut self) -> &mut Self where T: Default, { - self.or_insert(T::default()) + self.or_insert_with(T::default) } /// [Insert](EntityCommands::insert) `T::from_world` into this entity, /// if `T` is not already present. + /// + /// `T::from_world` will only be invoked if the component will actually be inserted. #[track_caller] pub fn or_from_world(&mut self) -> &mut Self where @@ -2237,6 +2432,12 @@ mod tests { } } + impl Default for W { + fn default() -> Self { + unreachable!() + } + } + #[test] fn entity_commands_entry() { let mut world = World::default(); @@ -2276,6 +2477,17 @@ mod tests { let id = commands.entity(entity).entry::>().entity().id(); queue.apply(&mut world); assert_eq!(id, entity); + let mut commands = Commands::new(&mut queue, &world); + commands + .entity(entity) + .entry::>() + .or_insert_with(|| W(5)) + .or_insert_with(|| unreachable!()) + .or_try_insert_with(|| unreachable!()) + .or_default() + .or_from_world(); + queue.apply(&mut world); + assert_eq!(5, world.get::>(entity).unwrap().0); } #[test] @@ -2286,7 +2498,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) @@ -2573,4 +2785,17 @@ mod tests { assert!(world.contains_resource::>()); assert!(world.contains_resource::>()); } + + #[test] + fn track_spawn_ticks() { + let mut world = World::default(); + world.increment_change_tick(); + let expected = world.change_tick(); + let id = world.commands().spawn_empty().id(); + world.flush(); + assert_eq!( + Some(expected), + world.entities().entity_get_spawned_or_despawned_at(id) + ); + } } diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 9107993f95..241f9955df 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,20 +1,21 @@ use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + component::{CheckChangeTicks, ComponentId, Tick}, + error::Result, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ - check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, IntoSystem, - System, SystemIn, SystemInput, SystemMeta, + check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, IntoResult, + IntoSystem, System, SystemIn, SystemInput, SystemMeta, }, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; -use super::SystemParamValidationError; +use super::{RunSystemError, SystemParamValidationError, SystemStateFlags}; /// A function system that runs with exclusive [`World`] access. /// @@ -22,18 +23,20 @@ use super::SystemParamValidationError; /// [`ExclusiveSystemParam`]s. /// /// [`ExclusiveFunctionSystem`] must be `.initialized` before they can be run. -pub struct ExclusiveFunctionSystem +pub struct ExclusiveFunctionSystem where F: ExclusiveSystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, param_state: Option<::State>, system_meta: SystemMeta, // NOTE: PhantomData T> gives this safe Send/Sync impls - marker: PhantomData Marker>, + marker: PhantomData (Marker, Out)>, } -impl ExclusiveFunctionSystem +impl ExclusiveFunctionSystem where F: ExclusiveSystemParamFunction, { @@ -41,7 +44,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 } } @@ -50,15 +53,22 @@ where #[doc(hidden)] pub struct IsExclusiveFunctionSystem; -impl IntoSystem for F +impl IntoSystem for F where + Out: 'static, Marker: 'static, + F::Out: IntoResult, F: ExclusiveSystemParamFunction, { - type System = ExclusiveFunctionSystem; + type System = ExclusiveFunctionSystem; fn into_system(func: Self) -> Self::System { ExclusiveFunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current( + >::run, + ) + .ptr_address(), param_state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -68,51 +78,28 @@ where const PARAM_MESSAGE: &str = "System's param_state was not found. Did you forget to initialize this system before running it?"; -impl System for ExclusiveFunctionSystem +impl System for ExclusiveFunctionSystem where Marker: 'static, + Out: 'static, + F::Out: IntoResult, F: ExclusiveSystemParamFunction, { type In = F::In; - type Out = F::Out; + type Out = Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - - #[inline] - fn archetype_component_access(&self) -> &Access { - &self.system_meta.archetype_component_access - } - - #[inline] - fn is_send(&self) -> bool { - // exclusive systems should have access to non-send resources + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred // the executor runs exclusive systems on the main thread, so this // field reflects that constraint - false - } - - #[inline] - fn is_exclusive(&self) -> bool { - true - } - - #[inline] - fn has_deferred(&self) -> bool { // exclusive systems have no deferred system params - false + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } #[inline] @@ -120,7 +107,7 @@ where &mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell, - ) -> Self::Out { + ) -> Result { // SAFETY: The safety is upheld by the caller. let world = unsafe { world.world_mut() }; world.last_change_tick_scope(self.system_meta.last_run, |world| { @@ -131,15 +118,40 @@ where self.param_state.as_mut().expect(PARAM_MESSAGE), &self.system_meta, ); + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = + subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, world, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(world, input, params); world.flush(); self.system_meta.last_run = world.increment_change_tick(); - out + IntoResult::into_result(out) }) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run) + .ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; + } + #[inline] fn apply_deferred(&mut self, _world: &mut World) { // "pure" exclusive systems do not have any buffers to apply. @@ -164,19 +176,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() } - fn update_archetype_component_access(&mut self, _world: UnsafeWorldCell) {} - #[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/exclusive_system_param.rs b/crates/bevy_ecs/src/system/exclusive_system_param.rs index f271e32e2f..f87182ab1f 100644 --- a/crates/bevy_ecs/src/system/exclusive_system_param.rs +++ b/crates/bevy_ecs/src/system/exclusive_system_param.rs @@ -4,7 +4,7 @@ use crate::{ system::{Local, SystemMeta, SystemParam, SystemState}, world::World, }; -use bevy_utils::synccell::SyncCell; +use bevy_platform::cell::SyncCell; use core::marker::PhantomData; use variadics_please::all_tuples; diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 5cf3fe2a44..35d7e709e9 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,8 +1,9 @@ use crate::{ - archetype::{ArchetypeComponentId, ArchetypeGeneration}, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, + error::{BevyError, Result}, + never::Never, prelude::FromWorld, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ReadOnlySystemParam, System, SystemIn, SystemInput, SystemParam, @@ -12,36 +13,25 @@ use crate::{ }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; #[cfg(feature = "trace")] use tracing::{info_span, Span}; -use super::{IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError}; +use super::{ + IntoSystem, ReadOnlySystem, RunSystemError, SystemParamBuilder, SystemParamValidationError, + SystemStateFlags, +}; /// 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, - /// This [`Access`] is used to determine which systems can run in parallel with each other - /// in the multithreaded executor. - /// - /// We use a [`ArchetypeComponentId`] as it is more precise than just checking [`ComponentId`]: - /// for example if you have one system with `Query<&mut T, With>` and one system with `Query<&mut T, With>` - /// they conflict if you just look at the [`ComponentId`] of `T`; but if there are no archetypes with - /// both `A`, `B` and `T` then in practice there's no risk of conflict. By using [`ArchetypeComponentId`] - /// we can be more precise because we can check if the existing archetypes of the [`World`] - /// cause a conflict - pub(crate) archetype_component_access: Access, + pub(crate) name: DebugName, // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other - is_send: bool, - has_deferred: bool, + flags: SystemStateFlags, pub(crate) last_run: Tick, #[cfg(feature = "trace")] pub(crate) system_span: Span, @@ -51,24 +41,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(), - archetype_component_access: Access::default(), - component_access_set: FilteredAccessSet::default(), - is_send: true, - has_deferred: false, + #[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 } @@ -84,13 +71,13 @@ 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`]. #[inline] pub fn is_send(&self) -> bool { - self.is_send + !self.flags.intersects(SystemStateFlags::NON_SEND) } /// Sets the system to be not [`Send`]. @@ -98,69 +85,20 @@ impl SystemMeta { /// This is irreversible. #[inline] pub fn set_non_send(&mut self) { - self.is_send = false; + self.flags |= SystemStateFlags::NON_SEND; } /// Returns true if the system has deferred [`SystemParam`]'s #[inline] pub fn has_deferred(&self) -> bool { - self.has_deferred + self.flags.intersects(SystemStateFlags::DEFERRED) } /// Marks the system as having deferred buffers like [`Commands`](`super::Commands`) /// This lets the scheduler insert [`ApplyDeferred`](`crate::prelude::ApplyDeferred`) systems automatically. #[inline] pub fn set_has_deferred(&mut self) { - self.has_deferred = true; - } - - /// Archetype component access that is used to determine which systems can run in parallel with each other - /// in the multithreaded executor. - /// - /// We use an [`ArchetypeComponentId`] as it is more precise than just checking [`ComponentId`]: - /// for example if you have one system with `Query<&mut A, With`, and one system with `Query<&mut A, Without`, - /// they conflict if you just look at the [`ComponentId`]; - /// but no archetype that matches the first query will match the second and vice versa, - /// which means there's no risk of conflict. - #[inline] - pub fn archetype_component_access(&self) -> &Access { - &self.archetype_component_access - } - - /// Returns a mutable reference to the [`Access`] for [`ArchetypeComponentId`]. - /// This is used to determine which systems can run in parallel with each other - /// in the multithreaded executor. - /// - /// We use an [`ArchetypeComponentId`] as it is more precise than just checking [`ComponentId`]: - /// for example if you have one system with `Query<&mut A, With`, and one system with `Query<&mut A, Without`, - /// they conflict if you just look at the [`ComponentId`]; - /// but no archetype that matches the first query will match the second and vice versa, - /// which means there's no risk of conflict. - /// - /// # Safety - /// - /// No access can be removed from the returned [`Access`]. - #[inline] - pub unsafe fn archetype_component_access_mut(&mut self) -> &mut Access { - &mut self.archetype_component_access - } - - /// 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 + self.flags |= SystemStateFlags::DEFERRED; } } @@ -197,7 +135,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); @@ -230,7 +168,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// #[derive(Resource)] /// struct CachedSystemState { @@ -260,7 +198,6 @@ pub struct SystemState { meta: SystemMeta, param_state: Param::State, world_id: WorldId, - archetype_generation: ArchetypeGeneration, } // Allow closure arguments to be inferred. @@ -276,15 +213,16 @@ macro_rules! impl_build_system { /// This method signature allows type inference of closure parameters for a system with no input. /// You can use [`SystemState::build_system_with_input()`] if you have input, or [`SystemState::build_any_system()`] if you don't need type inference. pub fn build_system< + InnerOut: IntoResult, Out: 'static, Marker, - F: FnMut($(SystemParamItem<$param>),*) -> Out - + SystemParamFunction + F: FnMut($(SystemParamItem<$param>),*) -> InnerOut + + SystemParamFunction > ( self, func: F, - ) -> FunctionSystem + ) -> FunctionSystem { self.build_any_system(func) } @@ -294,14 +232,15 @@ macro_rules! impl_build_system { /// You can use [`SystemState::build_system()`] if you have no input, or [`SystemState::build_any_system()`] if you don't need type inference. pub fn build_system_with_input< Input: SystemInput, + InnerOut: IntoResult, Out: 'static, Marker, - F: FnMut(Input, $(SystemParamItem<$param>),*) -> Out - + SystemParamFunction, + F: FnMut(Input, $(SystemParamItem<$param>),*) -> InnerOut + + SystemParamFunction, >( self, func: F, - ) -> FunctionSystem { + ) -> FunctionSystem { self.build_any_system(func) } } @@ -318,21 +257,18 @@ all_tuples!( impl SystemState { /// Creates a new [`SystemState`] with default state. - /// - /// ## Note - /// For users of [`SystemState::get_manual`] or [`get_manual_mut`](SystemState::get_manual_mut): - /// - /// `new` does not cache any of the world's archetypes, so you must call [`SystemState::update_archetypes`] - /// manually before calling `get_manual{_mut}`. 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, world_id: world.id(), - archetype_generation: ArchetypeGeneration::initial(), } } @@ -340,30 +276,35 @@ 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, world_id: world.id(), - archetype_generation: ArchetypeGeneration::initial(), } } /// Create a [`FunctionSystem`] from a [`SystemState`]. /// This method signature allows any system function, but the compiler will not perform type inference on closure parameters. /// You can use [`SystemState::build_system()`] or [`SystemState::build_system_with_input()`] to get type inference on parameters. - pub fn build_any_system>( - self, - func: F, - ) -> FunctionSystem { + pub fn build_any_system(self, func: F) -> FunctionSystem + where + F: SystemParamFunction>, + { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: Some(FunctionSystemState { param: self.param_state, world_id: self.world_id, }), system_meta: self.meta, - archetype_generation: self.archetype_generation, marker: PhantomData, } } @@ -387,19 +328,17 @@ impl SystemState { Param: ReadOnlySystemParam, { self.validate_world(world.id()); - self.update_archetypes(world); // SAFETY: Param is read-only and doesn't allow mutable access to World. // It also matches the World this SystemState was created with. - unsafe { self.get_unchecked_manual(world.as_unsafe_world_cell_readonly()) } + unsafe { self.get_unchecked(world.as_unsafe_world_cell_readonly()) } } /// Retrieve the mutable [`SystemParam`] values. #[inline] pub fn get_mut<'w, 's>(&'s mut self, world: &'w mut World) -> SystemParamItem<'w, 's, Param> { self.validate_world(world.id()); - self.update_archetypes(world); // SAFETY: World is uniquely borrowed and matches the World this SystemState was created with. - unsafe { self.get_unchecked_manual(world.as_unsafe_world_cell()) } + unsafe { self.get_unchecked(world.as_unsafe_world_cell()) } } /// Applies all state queued up for [`SystemParam`] values. For example, this will apply commands queued up @@ -415,14 +354,14 @@ impl SystemState { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have read-only access to - /// world data in `archetype_component_access`. + /// world data in `component_access_set`. /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). pub unsafe fn validate_param( - state: &Self, + state: &mut Self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { // SAFETY: Delegated to existing `SystemParam` implementations. - unsafe { Param::validate_param(&state.param_state, &state.meta, world) } + unsafe { Param::validate_param(&mut state.param_state, &state.meta, world) } } /// Returns `true` if `world_id` matches the [`World`] that was used to call [`SystemState::new`]. @@ -448,89 +387,68 @@ impl SystemState { } } - /// Updates the state's internal view of the [`World`]'s archetypes. If this is not called before fetching the parameters, - /// the results may not accurately reflect what is in the `world`. - /// - /// This is only required if [`SystemState::get_manual`] or [`SystemState::get_manual_mut`] is being called, and it only needs to - /// be called if the `world` has been structurally mutated (i.e. added/removed a component or resource). Users using - /// [`SystemState::get`] or [`SystemState::get_mut`] do not need to call this as it will be automatically called for them. + /// Has no effect #[inline] - pub fn update_archetypes(&mut self, world: &World) { - self.update_archetypes_unsafe_world_cell(world.as_unsafe_world_cell_readonly()); - } + #[deprecated( + since = "0.17.0", + note = "No longer has any effect. Calls may be removed." + )] + pub fn update_archetypes(&mut self, _world: &World) {} - /// Updates the state's internal view of the `world`'s archetypes. If this is not called before fetching the parameters, - /// the results may not accurately reflect what is in the `world`. - /// - /// This is only required if [`SystemState::get_manual`] or [`SystemState::get_manual_mut`] is being called, and it only needs to - /// be called if the `world` has been structurally mutated (i.e. added/removed a component or resource). Users using - /// [`SystemState::get`] or [`SystemState::get_mut`] do not need to call this as it will be automatically called for them. - /// - /// # Note - /// - /// This method only accesses world metadata. + /// Has no effect #[inline] - pub fn update_archetypes_unsafe_world_cell(&mut self, world: UnsafeWorldCell) { - assert_eq!(self.world_id, world.id(), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); + #[deprecated( + since = "0.17.0", + note = "No longer has any effect. Calls may be removed." + )] + pub fn update_archetypes_unsafe_world_cell(&mut self, _world: UnsafeWorldCell) {} - let archetypes = world.archetypes(); - let old_generation = - core::mem::replace(&mut self.archetype_generation, archetypes.generation()); - - for archetype in &archetypes[old_generation..] { - // SAFETY: The assertion above ensures that the param_state was initialized from `world`. - unsafe { Param::new_archetype(&mut self.param_state, archetype, &mut self.meta) }; - } - } - - /// Retrieve the [`SystemParam`] values. This can only be called when all parameters are read-only. - /// This will not update the state's view of the world's archetypes automatically nor increment the - /// world's change tick. - /// - /// For this to return accurate results, ensure [`SystemState::update_archetypes`] is called before this - /// function. - /// - /// Users should strongly prefer to use [`SystemState::get`] over this function. + /// Identical to [`SystemState::get`]. #[inline] + #[deprecated(since = "0.17.0", note = "Call `SystemState::get` instead.")] pub fn get_manual<'w, 's>(&'s mut self, world: &'w World) -> SystemParamItem<'w, 's, Param> where Param: ReadOnlySystemParam, { - self.validate_world(world.id()); - let change_tick = world.read_change_tick(); - // SAFETY: Param is read-only and doesn't allow mutable access to World. - // It also matches the World this SystemState was created with. - unsafe { self.fetch(world.as_unsafe_world_cell_readonly(), change_tick) } + self.get(world) } - /// Retrieve the mutable [`SystemParam`] values. This will not update the state's view of the world's archetypes - /// automatically nor increment the world's change tick. - /// - /// For this to return accurate results, ensure [`SystemState::update_archetypes`] is called before this - /// function. - /// - /// Users should strongly prefer to use [`SystemState::get_mut`] over this function. + /// Identical to [`SystemState::get_mut`]. #[inline] + #[deprecated(since = "0.17.0", note = "Call `SystemState::get_mut` instead.")] pub fn get_manual_mut<'w, 's>( &'s mut self, world: &'w mut World, ) -> SystemParamItem<'w, 's, Param> { - self.validate_world(world.id()); - let change_tick = world.change_tick(); - // SAFETY: World is uniquely borrowed and matches the World this SystemState was created with. - unsafe { self.fetch(world.as_unsafe_world_cell(), change_tick) } + self.get_mut(world) } - /// Retrieve the [`SystemParam`] values. This will not update archetypes automatically. + /// Identical to [`SystemState::get_unchecked`]. /// /// # Safety /// This call might access any of the input parameters in a way that violates Rust's mutability rules. Make sure the data /// access is safe in the context of global [`World`] access. The passed-in [`World`] _must_ be the [`World`] the [`SystemState`] was /// created with. #[inline] + #[deprecated(since = "0.17.0", note = "Call `SystemState::get_unchecked` instead.")] pub unsafe fn get_unchecked_manual<'w, 's>( &'s mut self, world: UnsafeWorldCell<'w>, + ) -> SystemParamItem<'w, 's, Param> { + // SAFETY: Caller ensures safety requirements + unsafe { self.get_unchecked(world) } + } + + /// Retrieve the [`SystemParam`] values. + /// + /// # Safety + /// This call might access any of the input parameters in a way that violates Rust's mutability rules. Make sure the data + /// access is safe in the context of global [`World`] access. The passed-in [`World`] _must_ be the [`World`] the [`SystemState`] was + /// created with. + #[inline] + pub unsafe fn get_unchecked<'w, 's>( + &'s mut self, + world: UnsafeWorldCell<'w>, ) -> SystemParamItem<'w, 's, Param> { let change_tick = world.increment_change_tick(); // SAFETY: The invariants are upheld by the caller. @@ -567,8 +485,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 } @@ -590,16 +507,17 @@ impl FromWorld for SystemState { /// /// The [`Clone`] implementation for [`FunctionSystem`] returns a new instance which /// is NOT initialized. The cloned system must also be `.initialized` before it can be run. -pub struct FunctionSystem +pub struct FunctionSystem where F: SystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, state: Option>, system_meta: SystemMeta, - archetype_generation: ArchetypeGeneration, // NOTE: PhantomData T> gives this safe Send/Sync impls - marker: PhantomData Marker>, + marker: PhantomData (Marker, Out)>, } /// The state of a [`FunctionSystem`], which must be initialized with @@ -609,12 +527,12 @@ struct FunctionSystemState { /// The cached state of the system's [`SystemParam`]s. param: P::State, /// The id of the [`World`] this system was initialized with. If the world - /// passed to [`System::update_archetype_component_access`] does not match + /// passed to [`System::run_unsafe`] or [`System::validate_param_unsafe`] does not match /// this id, a panic will occur. world_id: WorldId, } -impl FunctionSystem +impl FunctionSystem where F: SystemParamFunction, { @@ -628,16 +546,18 @@ where } // De-initializes the cloned system. -impl Clone for FunctionSystem +impl Clone for FunctionSystem where F: SystemParamFunction + Clone, { fn clone(&self) -> Self { Self { func: self.func.clone(), + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), - archetype_generation: ArchetypeGeneration::initial(), marker: PhantomData, } } @@ -647,24 +567,68 @@ where #[doc(hidden)] pub struct IsFunctionSystem; -impl IntoSystem for F +impl IntoSystem for F where + Out: 'static, Marker: 'static, - F: SystemParamFunction, + F: SystemParamFunction>, { - type System = FunctionSystem; + type System = FunctionSystem; fn into_system(func: Self) -> Self::System { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), - archetype_generation: ArchetypeGeneration::initial(), marker: PhantomData, } } } -impl FunctionSystem +/// A type that may be converted to the output of a [`System`]. +/// This is used to allow systems to return either a plain value or a [`Result`]. +pub trait IntoResult: Sized { + /// Converts this type into the system output type. + fn into_result(self) -> Result; +} + +impl IntoResult for T { + fn into_result(self) -> Result { + Ok(self) + } +} + +impl IntoResult for Result { + fn into_result(self) -> Result { + self + } +} + +impl IntoResult for Result { + fn into_result(self) -> Result { + Ok(self?) + } +} + +// The `!` impl can't be generic in `Out`, since that would overlap with +// `impl IntoResult for T` when `T` = `!`. +// Use explicit impls for `()` and `bool` so diverging functions +// can be used for systems and conditions. +impl IntoResult<()> for Never { + fn into_result(self) -> Result<(), RunSystemError> { + self + } +} + +impl IntoResult for Never { + fn into_result(self) -> Result { + self + } +} + +impl FunctionSystem where F: SystemParamFunction, { @@ -675,47 +639,23 @@ where "System's state was not found. Did you forget to initialize this system before running it?"; } -impl System for FunctionSystem +impl System for FunctionSystem where Marker: 'static, - F: SystemParamFunction, + Out: 'static, + F: SystemParamFunction>, { type In = F::In; - type Out = F::Out; + type Out = Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - - #[inline] - fn archetype_component_access(&self) -> &Access { - &self.system_meta.archetype_component_access - } - - #[inline] - fn is_send(&self) -> bool { - self.system_meta.is_send - } - - #[inline] - fn is_exclusive(&self) -> bool { - false - } - - #[inline] - fn has_deferred(&self) -> bool { - self.system_meta.has_deferred + fn flags(&self) -> SystemStateFlags { + self.system_meta.flags } #[inline] @@ -723,23 +663,47 @@ where &mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell, - ) -> Self::Out { + ) -> Result { #[cfg(feature = "trace")] let _span_guard = self.system_meta.system_span.enter(); let change_tick = world.increment_change_tick(); - let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; + let state = self.state.as_mut().expect(Self::ERROR_UNINITIALIZED); + assert_eq!(state.world_id, world.id(), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); // SAFETY: - // - The caller has invoked `update_archetype_component_access`, which will panic - // if the world does not match. + // - The above assert ensures the world matches. // - All world accesses used by `F::Param` have been registered, so the caller // will ensure that there are no data access conflicts. let params = - unsafe { F::Param::get_param(param_state, &self.system_meta, world, change_tick) }; + unsafe { F::Param::get_param(&mut state.param, &self.system_meta, world, change_tick) }; + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(input, params); + self.system_meta.last_run = change_tick; - out + IntoResult::into_result(out) + } + + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run).ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; } #[inline] @@ -759,52 +723,45 @@ where &mut self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - let param_state = &self.state.as_ref().expect(Self::ERROR_UNINITIALIZED).param; + let state = self.state.as_mut().expect(Self::ERROR_UNINITIALIZED); + assert_eq!(state.world_id, world.id(), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); // SAFETY: - // - The caller has invoked `update_archetype_component_access`, which will panic - // if the world does not match. + // - The above assert ensures the world matches. // - All world accesses used by `F::Param` have been registered, so the caller // will ensure that there are no data access conflicts. - unsafe { F::Param::validate_param(param_state, &self.system_meta, world) } + unsafe { F::Param::validate_param(&mut state.param, &self.system_meta, world) } } #[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); - } - - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - let state = self.state.as_mut().expect(Self::ERROR_UNINITIALIZED); - assert_eq!(state.world_id, world.id(), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); - - let archetypes = world.archetypes(); - let old_generation = - core::mem::replace(&mut self.archetype_generation, archetypes.generation()); - - for archetype in &archetypes[old_generation..] { - // SAFETY: The assertion above ensures that the param_state was initialized from `world`. - unsafe { F::Param::new_archetype(&mut state.param, archetype, &mut self.system_meta) }; - } + 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(), ); } @@ -823,10 +780,11 @@ where } /// SAFETY: `F`'s param is [`ReadOnlySystemParam`], so this system will only read from the world. -unsafe impl ReadOnlySystem for FunctionSystem +unsafe impl ReadOnlySystem for FunctionSystem where Marker: 'static, - F: SystemParamFunction, + Out: 'static, + F: SystemParamFunction>, F::Param: ReadOnlySystemParam, { } @@ -876,7 +834,7 @@ where /// // pipe the `parse_message_system`'s output into the `filter_system`s input /// let mut piped_system = IntoSystem::into_system(pipe(parse_message, filter)); /// piped_system.initialize(&mut world); -/// assert_eq!(piped_system.run((), &mut world), Some(42)); +/// assert_eq!(piped_system.run((), &mut world).unwrap(), Some(42)); /// } /// /// #[derive(Resource)] diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 12087fdf6a..c8d799b05d 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 /// @@ -80,7 +80,7 @@ pub type SystemIn<'a, S> = <::In as SystemInput>::Inner<'a>; /// let mut square_system = IntoSystem::into_system(square); /// square_system.initialize(&mut world); /// -/// assert_eq!(square_system.run(12, &mut world), 144); +/// assert_eq!(square_system.run(12, &mut world).unwrap(), 144); /// ``` /// /// [`SystemParam`]: crate::system::SystemParam @@ -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 @@ -318,9 +318,9 @@ mod tests { let mut a = 12; let b = 24; - assert_eq!(by_value.run((a, b), &mut world), 36); - assert_eq!(by_ref.run((&a, &b), &mut world), 36); - by_mut.run((&mut a, b), &mut world); + assert_eq!(by_value.run((a, b), &mut world).unwrap(), 36); + assert_eq!(by_ref.run((&a, &b), &mut world).unwrap(), 36); + by_mut.run((&mut a, b), &mut world).unwrap(); assert_eq!(a, 36); } } diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 011c220c85..26c767b051 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -97,7 +97,7 @@ //! - [`EventWriter`](crate::event::EventWriter) //! - [`NonSend`] and `Option` //! - [`NonSendMut`] and `Option` -//! - [`RemovedComponents`](crate::removal_detection::RemovedComponents) +//! - [`RemovedComponents`](crate::lifecycle::RemovedComponents) //! - [`SystemName`] //! - [`SystemChangeTick`] //! - [`Archetypes`](crate::archetype::Archetypes) (Provides Archetype metadata) @@ -390,7 +390,7 @@ pub fn assert_system_does_not_conflict>( @@ -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,10 +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().get_conflicts(y.component_access()); + let conflicts = x_access.get_conflicts(&y_access); let b_id = world .components() .get_resource_id(TypeId::of::()) @@ -1192,11 +1192,11 @@ mod tests { let mut without_filter = IntoSystem::into_system(without_filter); without_filter.initialize(&mut world); - without_filter.run((), &mut world); + without_filter.run((), &mut world).unwrap(); let mut with_filter = IntoSystem::into_system(with_filter); with_filter.initialize(&mut world); - with_filter.run((), &mut world); + with_filter.run((), &mut world).unwrap(); } #[test] @@ -1587,68 +1587,6 @@ mod tests { } } - #[test] - fn update_archetype_component_access_works() { - use std::collections::HashSet; - - fn a_not_b_system(_query: Query<&A, Without>) {} - - let mut world = World::default(); - let mut system = IntoSystem::into_system(a_not_b_system); - let mut expected_ids = HashSet::::new(); - let a_id = world.register_component::(); - - // set up system and verify its access is empty - system.initialize(&mut world); - system.update_archetype_component_access(world.as_unsafe_world_cell()); - let archetype_component_access = system.archetype_component_access(); - assert!(expected_ids - .iter() - .all(|id| archetype_component_access.has_component_read(*id))); - - // add some entities with archetypes that should match and save their ids - expected_ids.insert( - world - .spawn(A) - .archetype() - .get_archetype_component_id(a_id) - .unwrap(), - ); - expected_ids.insert( - world - .spawn((A, C)) - .archetype() - .get_archetype_component_id(a_id) - .unwrap(), - ); - - // add some entities with archetypes that should not match - world.spawn((A, B)); - world.spawn((B, C)); - - // update system and verify its accesses are correct - system.update_archetype_component_access(world.as_unsafe_world_cell()); - let archetype_component_access = system.archetype_component_access(); - assert!(expected_ids - .iter() - .all(|id| archetype_component_access.has_component_read(*id))); - - // one more round - expected_ids.insert( - world - .spawn((A, D)) - .archetype() - .get_archetype_component_id(a_id) - .unwrap(), - ); - world.spawn((A, B, D)); - system.update_archetype_component_access(world.as_unsafe_world_cell()); - let archetype_component_access = system.archetype_component_access(); - assert!(expected_ids - .iter() - .all(|id| archetype_component_access.has_component_read(*id))); - } - #[test] fn commands_param_set() { // Regression test for #4676 @@ -1691,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://bevyengine.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://bevyengine.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://bevyengine.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://bevyengine.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); @@ -1860,29 +1786,33 @@ mod tests { let mut sys = IntoSystem::into_system(first.pipe(second)); sys.initialize(&mut world); - sys.run(default(), &mut world); + sys.run(default(), &mut world).unwrap(); // The second system should observe a change made in the first system. - let info = sys.run( - Info { - do_first: true, - ..default() - }, - &mut world, - ); + let info = sys + .run( + Info { + do_first: true, + ..default() + }, + &mut world, + ) + .unwrap(); assert!(!info.first_flag); assert!(info.second_flag); // When a change is made in the second system, the first system // should observe it the next time they are run. - let info1 = sys.run( - Info { - do_second: true, - ..default() - }, - &mut world, - ); - let info2 = sys.run(default(), &mut world); + let info1 = sys + .run( + Info { + do_second: true, + ..default() + }, + &mut world, + ) + .unwrap(); + let info2 = sys.run(default(), &mut world).unwrap(); assert!(!info1.first_flag); assert!(!info1.second_flag); assert!(info2.first_flag); @@ -1919,7 +1849,9 @@ mod tests { } #[test] - #[should_panic] + #[should_panic( + expected = "Encountered an error in system `bevy_ecs::system::tests::simple_fallible_system::sys`: error" + )] fn simple_fallible_system() { fn sys() -> Result { Err("error")?; @@ -1930,6 +1862,20 @@ mod tests { run_system(&mut world, sys); } + #[test] + #[should_panic( + expected = "Encountered an error in system `bevy_ecs::system::tests::simple_fallible_exclusive_system::sys`: error" + )] + fn simple_fallible_exclusive_system() { + fn sys(_world: &mut World) -> Result { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + run_system(&mut world, sys); + } + // Regression test for // https://github.com/bevyengine/bevy/issues/18778 // @@ -1959,18 +1905,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!() @@ -1979,7 +1923,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!() }); @@ -1994,7 +1937,7 @@ mod tests { let mut world = World::new(); let mut system = IntoSystem::into_system(sys.with_input(42)); system.initialize(&mut world); - system.run((), &mut world); + system.run((), &mut world).unwrap(); assert_eq!(*system.value(), 43); } @@ -2017,7 +1960,7 @@ mod tests { assert!(system.value().is_none()); system.initialize(&mut world); assert!(system.value().is_some()); - system.run((), &mut world); + system.run((), &mut world).unwrap(); assert_eq!(system.value().unwrap().0, 6); } } diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 9bd35c5361..862ebf71c7 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -1,28 +1,18 @@ -use alloc::{borrow::Cow, vec::Vec}; -use core::marker::PhantomData; - use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, - error::Result, - never::Never, - prelude::{Bundle, Trigger}, - query::{Access, FilteredAccessSet}, - schedule::{Fallible, Infallible}, - system::{input::SystemIn, System}, - world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, + prelude::{Bundle, On}, + system::System, }; -use super::{IntoSystem, SystemParamValidationError}; +use super::IntoSystem; -/// Implemented for [`System`]s that have a [`Trigger`] as the first argument. -pub trait ObserverSystem: - System, Out = Out> + Send + 'static +/// Implemented for [`System`]s that have [`On`] as the first argument. +pub trait ObserverSystem: + System, Out = Out> + Send + 'static { } impl ObserverSystem for T where - T: System, Out = Out> + Send + 'static + T: System, Out = Out> + Send + 'static { } @@ -36,9 +26,9 @@ 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 { +pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. type System: ObserverSystem; @@ -46,9 +36,9 @@ pub trait IntoObserverSystem: Send + 'st fn into_system(this: Self) -> Self::System; } -impl IntoObserverSystem for S +impl IntoObserverSystem for S where - S: IntoSystem, Out, M> + Send + 'static, + S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, E: 'static, B: Bundle, @@ -60,156 +50,11 @@ where } } -impl IntoObserverSystem for S -where - S: IntoSystem, (), M> + Send + 'static, - S::System: ObserverSystem, - E: Send + Sync + 'static, - B: Bundle, -{ - type System = InfallibleObserverWrapper; - - fn into_system(this: Self) -> Self::System { - InfallibleObserverWrapper::new(IntoSystem::into_system(this)) - } -} -impl IntoObserverSystem for S -where - S: IntoSystem, Never, M> + Send + 'static, - E: Send + Sync + 'static, - B: Bundle, -{ - type System = InfallibleObserverWrapper; - - fn into_system(this: Self) -> Self::System { - InfallibleObserverWrapper::new(IntoSystem::into_system(this)) - } -} - -/// A wrapper that converts an observer system that returns `()` into one that returns `Ok(())`. -pub struct InfallibleObserverWrapper { - observer: S, - _marker: PhantomData<(E, B, Out)>, -} - -impl InfallibleObserverWrapper { - /// Create a new `InfallibleObserverWrapper`. - pub fn new(observer: S) -> Self { - Self { - observer, - _marker: PhantomData, - } - } -} - -impl System for InfallibleObserverWrapper -where - S: ObserverSystem, - E: Send + Sync + 'static, - B: Bundle, - Out: Send + Sync + 'static, -{ - type In = Trigger<'static, E, B>; - type Out = Result; - - #[inline] - fn name(&self) -> Cow<'static, str> { - self.observer.name() - } - - #[inline] - fn component_access(&self) -> &Access { - self.observer.component_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.observer.component_access_set() - } - - #[inline] - fn archetype_component_access(&self) -> &Access { - self.observer.archetype_component_access() - } - - #[inline] - fn is_send(&self) -> bool { - self.observer.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.observer.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.observer.has_deferred() - } - - #[inline] - unsafe fn run_unsafe( - &mut self, - input: SystemIn<'_, Self>, - world: UnsafeWorldCell, - ) -> Self::Out { - self.observer.run_unsafe(input, world); - Ok(()) - } - - #[inline] - fn apply_deferred(&mut self, world: &mut World) { - self.observer.apply_deferred(world); - } - - #[inline] - fn queue_deferred(&mut self, world: DeferredWorld) { - self.observer.queue_deferred(world); - } - - #[inline] - unsafe fn validate_param_unsafe( - &mut self, - world: UnsafeWorldCell, - ) -> Result<(), SystemParamValidationError> { - self.observer.validate_param_unsafe(world) - } - - #[inline] - fn initialize(&mut self, world: &mut World) { - self.observer.initialize(world); - } - - #[inline] - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.observer.update_archetype_component_access(world); - } - - #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.observer.check_change_tick(change_tick); - } - - #[inline] - fn get_last_run(&self) -> Tick { - self.observer.get_last_run() - } - - #[inline] - fn set_last_run(&mut self, last_run: Tick) { - self.observer.set_last_run(last_run); - } - - fn default_system_sets(&self) -> Vec { - self.observer.default_system_sets() - } -} - #[cfg(test)] mod tests { use crate::{ event::Event, - observer::Trigger, + observer::On, system::{In, IntoSystem}, world::World, }; @@ -219,7 +64,7 @@ mod tests { #[test] fn test_piped_observer_systems_no_input() { - fn a(_: Trigger) {} + fn a(_: On) {} fn b() {} let mut world = World::new(); @@ -228,7 +73,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 c1d9e671a0..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, @@ -115,7 +117,7 @@ use core::{ /// ``` /// /// Note that the filter is `With`, not `With<&ComponentB>`. Unlike query data, `With` -/// does require components to be behind a reference. +/// does not require components to be behind a reference. /// /// ## `QueryData` or `QueryFilter` tuples /// @@ -209,7 +211,7 @@ use core::{ /// # #[derive(Component)] /// # struct ComponentB; /// # -/// // A queried items must contain `ComponentA`. If they also contain `ComponentB`, its value will +/// // Queried items must contain `ComponentA`. If they also contain `ComponentB`, its value will /// // be fetched as well. /// fn optional_component_query(query: Query<(&ComponentA, Option<&ComponentB>)>) { /// // ... @@ -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, >())), } @@ -2016,17 +2031,67 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.as_nop().get(entity).is_ok() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new [`Query`] giving more + /// restrictive access to the entities matched by the current query. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// A transmute is valid only if `NewD` has a subset of the read, write, and required access + /// of the current query. A precise description of the access required by each parameter + /// type is given in the table below, but typical uses are to: + /// * Remove components, e.g. `Query<(&A, &B)>` to `Query<&A>`. + /// * Retrieve an existing component with reduced or equal access, e.g. `Query<&mut A>` to `Query<&A>` + /// or `Query<&T>` to `Query>`. + /// * Add parameters with no new access, for example adding an `Entity` parameter. + /// + /// Note that since filter terms are dropped, non-archetypal filters like + /// [`Added`], [`Changed`] and [`Spawned`] will not be respected. To maintain or change filter + /// terms see [`Self::transmute_lens_filtered`]. + /// + /// |`QueryData` parameter type|Access required| + /// |----|----| + /// |[`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], [`PhantomData`]|No access| + /// |[`EntityMut`]|Read and write access to all components, but no required access| + /// |[`EntityRef`]|Read access to all components, but no required access| + /// |`&T`, [`Ref`]|Read and required access to `T`| + /// |`&mut T`, [`Mut`]|Read, write and required access to `T`| + /// |[`Option`], [`AnyOf<(D, ...)>`]|Read and write access to `T`, but no required access| + /// |Tuples of query data and
`#[derive(QueryData)]` structs|The union of the access of their subqueries| + /// |[`FilteredEntityRef`], [`FilteredEntityMut`]|Determined by the [`QueryBuilder`] used to construct them. Any query can be transmuted to them, and they will receive the access of the source query. When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data| + /// + /// `transmute_lens` drops filter terms, but [`Self::transmute_lens_filtered`] supports returning a [`QueryLens`] with a new + /// filter type - the access required by filter parameters are as follows. + /// + /// |`QueryFilter` parameter type|Access required| + /// |----|----| + /// |[`Added`], [`Changed`]|Read and required access to `T`| + /// |[`With`], [`Without`]|No access| + /// |[`Or<(T, ...)>`]|Read access of the subqueries, but no required access| + /// |Tuples of query filters and `#[derive(QueryFilter)]` structs|The union of the access of their subqueries| + /// + /// [`Added`]: crate::query::Added + /// [`Added`]: crate::query::Added + /// [`AnyOf<(D, ...)>`]: crate::query::AnyOf + /// [`&Archetype`]: crate::archetype::Archetype + /// [`Changed`]: crate::query::Changed + /// [`Changed`]: crate::query::Changed + /// [`EntityMut`]: crate::world::EntityMut + /// [`EntityLocation`]: crate::entity::EntityLocation + /// [`EntityRef`]: crate::world::EntityRef + /// [`FilteredEntityRef`]: crate::world::FilteredEntityRef + /// [`FilteredEntityMut`]: crate::world::FilteredEntityMut + /// [`Has`]: crate::query::Has + /// [`Mut`]: crate::world::Mut + /// [`Or<(T, ...)>`]: crate::query::Or + /// [`QueryBuilder`]: crate::query::QueryBuilder + /// [`Ref`]: crate::world::Ref + /// [`SpawnDetails`]: crate::query::SpawnDetails + /// [`Spawned`]: crate::query::Spawned + /// [`With`]: crate::query::With + /// [`Without`]: crate::query::Without /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `D` + /// This will panic if the access required by `NewD` is not a subset of that required by + /// the original fetch `D`. /// /// ## Example /// @@ -2065,30 +2130,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, - /// you can also make limited changes to the types of parameters. - /// The new query must have a subset of the *read*, *write*, and *required* access of the original query. - /// - /// * `&mut T` and [`Mut`](crate::change_detection::Mut) have read, write, and required access to `T` - /// * `&T` and [`Ref`](crate::change_detection::Ref) have read and required access to `T` - /// * [`Option`] and [`AnyOf<(D, ...)>`](crate::query::AnyOf) have the read and write access of the subqueries, but no required access - /// * Tuples of query data and `#[derive(QueryData)]` structs have the union of the access of their subqueries - /// * [`EntityMut`](crate::world::EntityMut) has read and write access to all components, but no required access - /// * [`EntityRef`](crate::world::EntityRef) has read access to all components, but no required access - /// * [`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, - /// so can be added to any query - /// * [`FilteredEntityRef`](crate::world::FilteredEntityRef) and [`FilteredEntityMut`](crate::world::FilteredEntityMut) - /// have access determined by the [`QueryBuilder`](crate::query::QueryBuilder) used to construct them. - /// Any query can be transmuted to them, and they will receive the access of the source query. - /// When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data. - /// * [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) filters have read and required access to `T` - /// * [`With`](crate::query::With) and [`Without`](crate::query::Without) filters have no access at all, - /// so can be added to any query - /// * Tuples of query filters and `#[derive(QueryFilter)]` structs have the union of the access of their subqueries - /// * [`Or<(F, ...)>`](crate::query::Or) filters have the read access of the subqueries, but no required access - /// /// ### Examples of valid transmutes /// /// ```rust @@ -2165,28 +2206,21 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// // Nested inside of an `Or` filter, they have the same access as `Option<&T>`. /// assert_valid_transmute_filtered::, (), Entity, Or<(Changed, With)>>(); /// ``` - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`SpawnDetails`]: crate::query::SpawnDetails - /// [`&Archetype`]: crate::archetype::Archetype - /// [`Has`]: crate::query::Has #[track_caller] pub fn transmute_lens(&mut self) -> QueryLens<'_, NewD> { self.transmute_lens_filtered::() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new `Query` giving more restrictive + /// access to the entities matched by the current query. + /// /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// See [`Self::transmute_lens`] for a description of allowed transmutes. /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `Q` + /// This will panic if `NewD` is not a subset of the original fetch `D` /// /// ## Example /// @@ -2225,22 +2259,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, you can also - /// make limited changes to the types of parameters. - /// - /// * Can always add/remove [`Entity`] - /// * Can always add/remove [`EntityLocation`] - /// * Can always add/remove [`&Archetype`] - /// * `Ref` <-> `&T` - /// * `&mut T` -> `&T` - /// * `&mut T` -> `Ref` - /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`&Archetype`]: crate::archetype::Archetype - /// /// # See also /// /// - [`transmute_lens`](Self::transmute_lens) to convert to a lens using a mutable borrow of the [`Query`]. @@ -2251,6 +2269,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens`] but also includes a [`QueryFilter`] type. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), @@ -2266,10 +2286,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens_inner`] but also includes a [`QueryFilter`] type. /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they + /// are in the type signature. /// /// # See also /// @@ -2443,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 { @@ -2456,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 { @@ -2465,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 { @@ -2565,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 } } @@ -2627,6 +2665,36 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Populated<'w, 's, D, F> { } } +impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Populated<'w, 's, D, F> { + type Item = as IntoIterator>::Item; + + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, 'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'a Populated<'w, 's, D, F> { + type Item = <&'a Query<'w, 's, D, F> as IntoIterator>::Item; + + type IntoIter = <&'a Query<'w, 's, D, F> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.deref().into_iter() + } +} + +impl<'a, 'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'a mut Populated<'w, 's, D, F> { + type Item = <&'a mut Query<'w, 's, D, F> as IntoIterator>::Item; + + type IntoIter = <&'a mut Query<'w, 's, D, F> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.deref_mut().into_iter() + } +} + #[cfg(test)] mod tests { use crate::{prelude::*, query::QueryEntityError}; diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 8ef7d9ed57..e2a853dbe9 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -1,122 +1,14 @@ -use alloc::{borrow::Cow, vec::Vec}; +use bevy_utils::prelude::DebugName; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, - query::{Access, FilteredAccessSet}, - system::{input::SystemIn, BoxedSystem, System, SystemInput}, + query::FilteredAccessSet, + system::{input::SystemIn, BoxedSystem, RunSystemError, System, SystemInput}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; -use super::{IntoSystem, SystemParamValidationError}; - -/// A wrapper system to change a system that returns `()` to return `Ok(())` to make it into a [`ScheduleSystem`] -pub struct InfallibleSystemWrapper>(S); - -impl> InfallibleSystemWrapper { - /// Create a new `OkWrapperSystem` - pub fn new(system: S) -> Self { - Self(IntoSystem::into_system(system)) - } -} - -impl> System for InfallibleSystemWrapper { - type In = (); - type Out = Result; - - #[inline] - fn name(&self) -> Cow<'static, str> { - self.0.name() - } - - #[inline] - fn component_access(&self) -> &Access { - self.0.component_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.0.component_access_set() - } - - #[inline(always)] - fn archetype_component_access(&self) -> &Access { - self.0.archetype_component_access() - } - - #[inline] - fn is_send(&self) -> bool { - self.0.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.0.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.0.has_deferred() - } - - #[inline] - unsafe fn run_unsafe( - &mut self, - input: SystemIn<'_, Self>, - world: UnsafeWorldCell, - ) -> Self::Out { - self.0.run_unsafe(input, world); - Ok(()) - } - - #[inline] - fn apply_deferred(&mut self, world: &mut World) { - self.0.apply_deferred(world); - } - - #[inline] - fn queue_deferred(&mut self, world: DeferredWorld) { - self.0.queue_deferred(world); - } - - #[inline] - unsafe fn validate_param_unsafe( - &mut self, - world: UnsafeWorldCell, - ) -> Result<(), SystemParamValidationError> { - self.0.validate_param_unsafe(world) - } - - #[inline] - fn initialize(&mut self, world: &mut World) { - self.0.initialize(world); - } - - #[inline] - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.0.update_archetype_component_access(world); - } - - #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.0.check_change_tick(change_tick); - } - - #[inline] - fn get_last_run(&self) -> Tick { - self.0.get_last_run() - } - - #[inline] - fn set_last_run(&mut self, last_run: Tick) { - self.0.set_last_run(last_run); - } - - fn default_system_sets(&self) -> Vec { - self.0.default_system_sets() - } -} +use super::{IntoSystem, SystemParamValidationError, SystemStateFlags}; /// See [`IntoSystem::with_input`] for details. pub struct WithInputWrapper @@ -158,45 +50,31 @@ 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(&self) -> &Access { - self.system.component_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - - fn archetype_component_access(&self) -> &Access { - self.system.archetype_component_access() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( &mut self, _input: SystemIn<'_, Self>, world: UnsafeWorldCell, - ) -> Self::Out { + ) -> Result { self.system.run_unsafe(&mut self.value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } @@ -212,16 +90,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 update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.system.update_archetype_component_access(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 { @@ -269,42 +143,22 @@ 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(&self) -> &Access { - self.system.component_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - - fn archetype_component_access(&self) -> &Access { - self.system.archetype_component_access() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( &mut self, _input: SystemIn<'_, Self>, world: UnsafeWorldCell, - ) -> Self::Out { + ) -> Result { let value = self .value .as_mut() @@ -312,6 +166,12 @@ where self.system.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } @@ -327,19 +187,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 update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.system.update_archetype_component_access(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 { @@ -352,4 +208,4 @@ where } /// Type alias for a `BoxedSystem` that a `Schedule` can store. -pub type ScheduleSystem = BoxedSystem<(), Result>; +pub type ScheduleSystem = BoxedSystem<(), ()>; diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 18ec7f44cd..aad37c09d0 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -2,24 +2,37 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17353." )] -use core::fmt::Debug; +use bevy_utils::prelude::DebugName; +use bitflags::bitflags; +use core::fmt::{Debug, Display}; use log::warn; -use thiserror::Error; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + component::{CheckChangeTicks, ComponentId, Tick}, + error::BevyError, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; -use alloc::{borrow::Cow, boxed::Box, vec::Vec}; -use core::any::TypeId; +use alloc::{boxed::Box, vec::Vec}; +use core::any::{Any, TypeId}; use super::{IntoSystem, SystemParamValidationError}; +bitflags! { + /// Bitflags representing system states and requirements. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct SystemStateFlags: u8 { + /// Set if system cannot be sent across threads + const NON_SEND = 1 << 0; + /// Set if system requires exclusive World access + const EXCLUSIVE = 1 << 1; + /// Set if system has deferred buffers. + const DEFERRED = 1 << 2; + } +} /// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule) /// /// Systems are functions with all arguments implementing @@ -37,30 +50,35 @@ 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 [`Access`]. - fn component_access(&self) -> &Access; + /// Returns the [`SystemStateFlags`] of the system. + fn flags(&self) -> SystemStateFlags; - /// Returns the system's component [`FilteredAccessSet`]. - fn component_access_set(&self) -> &FilteredAccessSet; - - /// Returns the system's archetype component [`Access`]. - fn archetype_component_access(&self) -> &Access; /// Returns true if the system is [`Send`]. - fn is_send(&self) -> bool; + #[inline] + fn is_send(&self) -> bool { + !self.flags().intersects(SystemStateFlags::NON_SEND) + } /// Returns true if the system must be run exclusively. - fn is_exclusive(&self) -> bool; + #[inline] + fn is_exclusive(&self) -> bool { + self.flags().intersects(SystemStateFlags::EXCLUSIVE) + } /// Returns true if system has deferred buffers. - fn has_deferred(&self) -> bool; + #[inline] + fn has_deferred(&self) -> bool { + self.flags().intersects(SystemStateFlags::DEFERRED) + } /// Runs the system with the given input in the world. Unlike [`System::run`], this function /// can be called in parallel with other systems and may break Rust's aliasing rules @@ -72,15 +90,19 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `archetype_component_access`. 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`. - /// - The method [`System::update_archetype_component_access`] must be called at some - /// point before this one, with the same exact [`World`]. If [`System::update_archetype_component_access`] - /// panics (or otherwise does not return for any reason), this method must not be called. - unsafe fn run_unsafe(&mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell) - -> Self::Out; + unsafe fn run_unsafe( + &mut self, + input: SystemIn<'_, Self>, + world: UnsafeWorldCell, + ) -> Result; + + /// Refresh the inner pointer based on the latest hot patch jump table + #[cfg(feature = "hotpatching")] + fn refresh_hotpatch(&mut self); /// Runs the system with the given input in the world. /// @@ -89,10 +111,14 @@ pub trait System: Send + Sync + 'static { /// Unlike [`System::run_unsafe`], this will apply deferred parameters *immediately*. /// /// [`run_readonly`]: ReadOnlySystem::run_readonly - fn run(&mut self, input: SystemIn<'_, Self>, world: &mut World) -> Self::Out { - let ret = self.run_without_applying_deferred(input, world); + fn run( + &mut self, + input: SystemIn<'_, Self>, + world: &mut World, + ) -> Result { + let ret = self.run_without_applying_deferred(input, world)?; self.apply_deferred(world); - ret + Ok(ret) } /// Runs the system with the given input in the world. @@ -102,9 +128,11 @@ pub trait System: Send + Sync + 'static { &mut self, input: SystemIn<'_, Self>, world: &mut World, - ) -> Self::Out { + ) -> Result { let world_cell = world.as_unsafe_world_cell(); - self.update_archetype_component_access(world_cell); + // SAFETY: + // - We have exclusive access to the entire world. + unsafe { self.validate_param_unsafe(world_cell) }?; // SAFETY: // - We have exclusive access to the entire world. // - `update_archetype_component_access` has been called. @@ -123,7 +151,7 @@ pub trait System: Send + Sync + 'static { /// Validates that all parameters can be acquired and that system can run without panic. /// Built-in executors use this to prevent invalid systems from running. /// - /// However calling and respecting [`System::validate_param_unsafe`] or it's safe variant + /// However calling and respecting [`System::validate_param_unsafe`] or its safe variant /// is not a strict requirement, both [`System::run`] and [`System::run_unsafe`] /// should provide their own safety mechanism to prevent undefined behavior. /// @@ -134,11 +162,8 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `archetype_component_access`. 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. - /// - The method [`System::update_archetype_component_access`] must be called at some - /// point before this one, with the same exact [`World`]. If [`System::update_archetype_component_access`] - /// panics (or otherwise does not return for any reason), this method must not be called. unsafe fn validate_param_unsafe( &mut self, world: UnsafeWorldCell, @@ -148,28 +173,21 @@ pub trait System: Send + Sync + 'static { /// that runs on exclusive, single-threaded `world` pointer. fn validate_param(&mut self, world: &World) -> Result<(), SystemParamValidationError> { let world_cell = world.as_unsafe_world_cell_readonly(); - self.update_archetype_component_access(world_cell); // SAFETY: // - We have exclusive access to the entire world. - // - `update_archetype_component_access` has been called. unsafe { self.validate_param_unsafe(world_cell) } } /// Initialize the system. - fn initialize(&mut self, _world: &mut World); - - /// Update the system's archetype component [`Access`]. /// - /// ## Note for implementers - /// `world` may only be used to access metadata. This can be done in safe code - /// via functions such as [`UnsafeWorldCell::archetypes`]. - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell); + /// 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). /// @@ -208,9 +226,15 @@ pub unsafe trait ReadOnlySystem: System { /// /// Unlike [`System::run`], this can be called with a shared reference to the world, /// since this system is known not to modify the world. - fn run_readonly(&mut self, input: SystemIn<'_, Self>, world: &World) -> Self::Out { + fn run_readonly( + &mut self, + input: SystemIn<'_, Self>, + world: &World, + ) -> Result { let world = world.as_unsafe_world_cell_readonly(); - self.update_archetype_component_access(world); + // SAFETY: + // - We have read-only access to the entire world. + unsafe { self.validate_param_unsafe(world) }?; // SAFETY: // - We have read-only access to the entire world. // - `update_archetype_component_access` has been called. @@ -221,9 +245,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.", @@ -373,28 +401,49 @@ impl RunSystemOnce for &mut World { { let mut system: T::System = IntoSystem::into_system(system); system.initialize(self); - system - .validate_param(self) - .map_err(|err| RunSystemError::InvalidParams { - system: system.name(), - err, - })?; - Ok(system.run(input, self)) + system.run(input, self) } } /// Running system failed. -#[derive(Error, Debug)] +#[derive(Debug)] pub enum RunSystemError { /// System could not be run due to parameters that failed validation. - /// This should not be considered an error if [`field@SystemParamValidationError::skipped`] is `true`. - #[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>, - /// The returned parameter validation error. - err: SystemParamValidationError, - }, + /// This is not considered an error. + Skipped(SystemParamValidationError), + /// System returned an error or failed required parameter validation. + Failed(BevyError), +} + +impl Display for RunSystemError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Skipped(err) => write!( + f, + "System did not run due to failed parameter validation: {err}" + ), + Self::Failed(err) => write!(f, "{err}"), + } + } +} + +impl From for RunSystemError +where + BevyError: From, +{ + fn from(mut value: E) -> Self { + // Specialize the impl so that a skipped `SystemParamValidationError` + // is converted to `Skipped` instead of `Failed`. + // Note that the `downcast_mut` check is based on the static type, + // and can be optimized out after monomorphization. + let any: &mut dyn Any = &mut value; + if let Some(err) = any.downcast_mut::() { + if err.skipped { + return Self::Skipped(core::mem::replace(err, SystemParamValidationError::EMPTY)); + } + } + Self::Failed(From::from(value)) + } } #[cfg(test)] @@ -473,8 +522,8 @@ mod tests { // This fails because `T` has not been added to the world yet. 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"; - assert_eq!(expected, result.unwrap_err().to_string()); + assert!(matches!(result, Err(RunSystemError::Failed { .. }))); + let expected = "Parameter `Res` failed validation: Resource does not exist\n"; + assert!(result.unwrap_err().to_string().contains(expected)); } } 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 6ee7e08fb5..d552bf1f1d 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -1,6 +1,6 @@ pub use crate::change_detection::{NonSendMut, Res, ResMut}; use crate::{ - archetype::{Archetype, Archetypes}, + archetype::Archetypes, bundle::Bundles, change_detection::{MaybeLocation, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Tick}, @@ -23,8 +23,9 @@ use alloc::{ vec::Vec, }; pub use bevy_ecs_macros::SystemParam; +use bevy_platform::cell::SyncCell; use bevy_ptr::UnsafeCellDeref; -use bevy_utils::synccell::SyncCell; +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,7 +151,8 @@ 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"; -/// assert!(err.to_string().ends_with(expected)); +/// # #[cfg(feature="Trace")] // Without debug_utils/debug enabled MyParam::foo is stripped and breaks the assert +/// assert!(err.to_string().contains(expected)); /// ``` /// /// ## Builders @@ -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,25 +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; - /// 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; + /// Creates a new instance of this param's [`State`](SystemParam::State). + fn init_state(world: &mut World) -> Self::State; - /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a - /// - /// # Safety - /// `archetype` must be from the [`World`] used to initialize `state` in [`SystemParam::init_state`]. - #[inline] - #[expect( - unused_variables, - reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." - )] - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + /// Registers any [`World`] access used by this [`SystemParam`] + 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). @@ -290,15 +282,14 @@ 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). - /// - All `world`'s archetypes have been processed by [`new_archetype`](SystemParam::new_archetype). #[expect( unused_variables, reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." )] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -310,9 +301,8 @@ 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). - /// - All `world`'s archetypes have been processed by [`new_archetype`](SystemParam::new_archetype). unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, @@ -336,24 +326,31 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re { } -// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If +// SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. unsafe impl SystemParam for Query<'_, '_, D, F> { 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_with_access(world, &mut system_meta.archetype_component_access); - init_query_param(world, system_meta, &state); - state + fn init_state(world: &mut World) -> Self::State { + QueryState::new(world) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - state.new_archetype(archetype, &mut system_meta.archetype_component_access); + 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] @@ -367,32 +364,14 @@ unsafe impl SystemParam for Qu // so the caller ensures that `world` has permission to access any // world data that the query needs. // The caller ensures the world matches the one used in init_state. - unsafe { state.query_unchecked_manual_with_ticks(world, system_meta.last_run, change_tick) } + unsafe { state.query_unchecked_with_ticks(world, system_meta.last_run, change_tick) } } } -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, @@ -406,26 +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://bevyengine.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 and ArchetypeComponentId access is applied to SystemMeta. If +// 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) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: Delegate to existing `SystemParam` implementations. - unsafe { Query::new_archetype(state, archetype, system_meta) }; + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -437,9 +418,8 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo ) -> Self::Item<'w, 's> { // SAFETY: State ensures that the components it accesses are not accessible somewhere elsewhere. // The caller ensures the world matches the one used in init_state. - let query = unsafe { - state.query_unchecked_manual_with_ticks(world, system_meta.last_run, change_tick) - }; + let query = + unsafe { state.query_unchecked_with_ticks(world, system_meta.last_run, change_tick) }; let single = query .single_inner() .expect("The query was expected to contain exactly one matching entity."); @@ -451,7 +431,7 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -459,11 +439,7 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo // and the query is read only. // The caller ensures the world matches the one used in init_state. let query = unsafe { - state.query_unchecked_manual_with_ticks( - world, - system_meta.last_run, - world.change_tick(), - ) + state.query_unchecked_with_ticks(world, system_meta.last_run, world.change_tick()) }; match query.single_inner() { Ok(_) => Ok(()), @@ -478,12 +454,12 @@ 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> { } -// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If +// SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. unsafe impl SystemParam for Populated<'_, '_, D, F> @@ -491,17 +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) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: Delegate to existing `SystemParam` implementations. - unsafe { Query::new_archetype(state, archetype, system_meta) }; + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -518,7 +494,7 @@ unsafe impl SystemParam #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -526,11 +502,7 @@ unsafe impl SystemParam // - We have read-only access to the components accessed by query. // - The caller ensures the world matches the one used in init_state. let query = unsafe { - state.query_unchecked_manual_with_ticks( - world, - system_meta.last_run, - world.change_tick(), - ) + state.query_unchecked_with_ticks(world, system_meta.last_run, world.change_tick()) }; if query.is_empty() { Err(SystemParamValidationError::skipped::( @@ -629,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 } @@ -669,13 +641,13 @@ 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,)* { } - // SAFETY: Relevant parameter ComponentId and ArchetypeComponentId access is applied to SystemMeta. If any ParamState conflicts + // SAFETY: Relevant parameter ComponentId access is applied to SystemMeta. If any ParamState conflicts // with any prior access, a panic will occur. unsafe impl<'_w, '_s, $($param: SystemParam,)*> SystemParam for ParamSet<'_w, '_s, ($($param,)*)> { @@ -690,34 +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 { - $( - // 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(); - $system_meta.archetype_component_access.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()); - )* - // 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); - system_meta - .archetype_component_access - .extend(&$system_meta.archetype_component_access); - )* - ($($param,)*) + fn init_state(world: &mut World) -> Self::State { + ($($param::init_state(world),)*) } - unsafe fn new_archetype(state: &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { <($($param,)*) as SystemParam>::new_archetype(state, archetype, system_meta); } + #[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; + $( + // 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); + )* + $( + // 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); + )* } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -730,7 +700,7 @@ macro_rules! impl_param_set { #[inline] unsafe fn validate_param<'w, 's>( - state: &'s Self::State, + state: &'s mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'w>, ) -> Result<(), SystemParamValidationError> { @@ -773,42 +743,41 @@ 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> {} -// SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res +// SAFETY: Res ComponentId access is applied to SystemMeta. If this Res // conflicts with any prior access, a panic will occur. 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::(); - let archetype_component_id = world.initialize_resource_internal(component_id).id(); + 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://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), + "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - system_meta - .archetype_component_access - .add_resource_read(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -840,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(), @@ -856,40 +825,38 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { } } -// SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res +// SAFETY: Res ComponentId access is applied to SystemMeta. If this Res // conflicts with any prior access, a panic will occur. 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::(); - let archetype_component_id = world.initialize_resource_internal(component_id).id(); + 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://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + 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://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - system_meta - .archetype_component_access - .add_resource_write(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -920,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::(), @@ -944,28 +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 { - let mut access = Access::default(); - access.read_all(); - if !system_meta - .archetype_component_access - .is_compatible(&access) - { - panic!("&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules"); - } - system_meta.archetype_component_access.extend(&access); + 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] @@ -985,17 +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(); - system_meta.archetype_component_access.write_all(); + component_access_set.write_all(); } unsafe fn get_param<'world, 'state>( @@ -1033,10 +999,10 @@ unsafe impl<'w> SystemParam for DeferredWorld<'w> { /// write_system.initialize(world); /// read_system.initialize(world); /// -/// assert_eq!(read_system.run((), world), 0); +/// assert_eq!(read_system.run((), world).unwrap(), 0); /// write_system.run((), world); /// // Note how the read local is still 0 due to the locals not being shared. -/// assert_eq!(read_system.run((), world), 0); +/// assert_eq!(read_system.run((), world).unwrap(), 0); /// ``` /// /// A simple way to set a different default value for a local is by wrapping the value with an Option. @@ -1053,9 +1019,9 @@ unsafe impl<'w> SystemParam for DeferredWorld<'w> { /// counter_system.initialize(world); /// /// // Counter is initialized at 10, and increases to 11 on first run. -/// assert_eq!(counter_system.run((), world), 11); +/// assert_eq!(counter_system.run((), world).unwrap(), 11); /// // Counter is only increased by 1 on subsequent runs. -/// assert_eq!(counter_system.run((), world), 12); +/// assert_eq!(counter_system.run((), world).unwrap(), 12); /// ``` /// /// N.B. A [`Local`]s value cannot be read or written to outside of the containing system. @@ -1125,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, @@ -1305,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); } @@ -1338,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(); } @@ -1411,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 { @@ -1426,39 +1416,37 @@ impl<'a, T> From> for NonSend<'a, T> { } } -// SAFETY: NonSendComponentId and ArchetypeComponentId access is applied to SystemMeta. If this +// SAFETY: NonSendComponentId access is applied to SystemMeta. If this // NonSend conflicts with any prior access, a panic will occur. 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 archetype_component_id = world.initialize_non_send_internal(component_id).id(); - - 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://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), + "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", + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - - system_meta - .archetype_component_access - .add_resource_read(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1490,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::() ) }); @@ -1504,42 +1492,40 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { } } -// SAFETY: NonSendMut ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this +// SAFETY: NonSendMut ComponentId access is applied to SystemMeta. If this // NonSendMut conflicts with any prior access, a panic will occur. 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 archetype_component_id = world.initialize_non_send_internal(component_id).id(); - - 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://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "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", + 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://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "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", + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - system_meta - .archetype_component_access - .add_resource_write(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1571,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(), @@ -1590,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>( @@ -1611,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>( @@ -1632,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>( @@ -1653,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>( @@ -1675,7 +1693,7 @@ unsafe impl<'a> SystemParam for &'a Bundles { /// Component change ticks that are more recent than `last_run` will be detected by the system. /// Those can be read by calling [`last_changed`](crate::change_detection::DetectChanges::last_changed) /// on a [`Mut`](crate::change_detection::Mut) or [`ResMut`](ResMut). -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct SystemChangeTick { last_run: Tick, this_run: Tick, @@ -1703,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>( @@ -1725,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] @@ -1741,15 +1776,6 @@ unsafe impl SystemParam for Option { .map(|()| T::get_param(state, system_meta, world, change_tick)) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) }; - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { T::apply(state, system_meta, world); } @@ -1768,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] @@ -1783,15 +1818,6 @@ unsafe impl SystemParam for Result 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] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1889,15 +1924,6 @@ unsafe impl SystemParam for When { When(T::get_param(state, system_meta, world, change_tick)) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) }; - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { T::apply(state, system_meta, world); } @@ -1910,21 +1936,31 @@ 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: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1944,23 +1980,12 @@ 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() } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - for state in state { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) }; - } - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { for state in state { T::apply(state, system_meta, world); @@ -1974,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, @@ -2001,17 +2046,6 @@ unsafe impl SystemParam for ParamSet<'_, '_, Vec> { } } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - for state in state { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) } - } - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { for state in state { T::apply(state, system_meta, world); @@ -2030,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 { @@ -2048,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) }, @@ -2082,18 +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),)*)) } - #[inline] - unsafe fn new_archetype(($($param,)*): &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { - #[allow( - unused_unsafe, - reason = "Zero-length tuples will not run anything in the unsafe block." - )] - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { $($param::new_archetype($param, archetype, system_meta);)* } + 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] @@ -2112,7 +2141,7 @@ macro_rules! impl_system_param_tuple { #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -2261,17 +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) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: The caller guarantees that the provided `archetype` matches the World used to initialize `state`. - unsafe { P::new_archetype(state, archetype, system_meta) }; + P::init_access(state, system_meta, component_access_set, world); } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -2284,7 +2313,7 @@ unsafe impl SystemParam for StaticSystemParam<'_, '_, #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -2308,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>( @@ -2510,16 +2547,7 @@ impl DynSystemParamState { } /// Allows a [`SystemParam::State`] to be used as a trait object for implementing [`DynSystemParam`]. -trait DynParamState: Sync + Send { - /// Casts the underlying `ParamState` to an `Any` so it can be downcast. - fn as_any_mut(&mut self) -> &mut dyn Any; - - /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a - /// - /// # Safety - /// `archetype` must be from the [`World`] used to initialize `state` in [`SystemParam::init_state`]. - unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta); - +trait DynParamState: Sync + Send + Any { /// Applies any deferred mutations stored in this [`SystemParam`]'s state. /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). /// @@ -2529,12 +2557,20 @@ trait DynParamState: Sync + Send { /// 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 /// Refer to [`SystemParam::validate_param`]. unsafe fn validate_param( - &self, + &mut self, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError>; @@ -2544,15 +2580,6 @@ trait DynParamState: Sync + Send { struct ParamState(T::State); impl DynParamState for ParamState { - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(&mut self.0, archetype, system_meta) }; - } - fn apply(&mut self, system_meta: &SystemMeta, world: &mut World) { T::apply(&mut self.0, system_meta, world); } @@ -2561,28 +2588,48 @@ impl DynParamState for ParamState { T::queue(&mut self.0, system_meta, world); } - unsafe fn validate_param( + 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, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - T::validate_param(&self.0, system_meta, world) + T::validate_param(&mut self.0, system_meta, world) } } -// 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: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -2597,27 +2644,11 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { change_tick: Tick, ) -> Self::Item<'world, 'state> { // SAFETY: - // - `state.0` is a boxed `ParamState`, and its implementation of `as_any_mut` returns `self`. - // - The state was obtained from `SystemParamBuilder::build()`, which registers all [`World`] accesses used - // by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). + // - `state.0` is a boxed `ParamState`. + // - `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_any_mut(), - world, - system_meta.clone(), - change_tick, - ) - } - } - - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { state.0.new_archetype(archetype, system_meta) }; + unsafe { DynSystemParam::new(state.0.as_mut(), world, system_meta.clone(), change_tick) } } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -2629,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) } } } @@ -2656,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) } } } @@ -2706,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. @@ -2738,10 +2821,17 @@ impl SystemParamValidationError { Self { skipped, message: message.into(), - param: Cow::Borrowed(core::any::type_name::()), + param: DebugName::type_name::(), field: field.into(), } } + + pub(crate) const EMPTY: Self = Self { + skipped: false, + message: Cow::Borrowed(""), + param: DebugName::borrowed(""), + field: Cow::Borrowed(""), + }; } impl Display for SystemParamValidationError { @@ -2749,17 +2839,21 @@ impl Display for SystemParamValidationError { write!( fmt, "Parameter `{}{}` failed validation: {}", - ShortName(&self.param), + self.param.shortname(), self.field, self.message - ) + )?; + if !self.skipped { + write!(fmt, "\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.")?; + } + Ok(()) } } #[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. @@ -2991,7 +3085,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; @@ -3005,11 +3099,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 cf53b35be5..bc87cd4fea 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -3,7 +3,10 @@ use crate::reflect::ReflectComponent; use crate::{ change_detection::Mut, entity::Entity, - system::{input::SystemInput, BoxedSystem, IntoSystem, SystemParamValidationError}, + error::BevyError, + system::{ + input::SystemInput, BoxedSystem, IntoSystem, RunSystemError, SystemParamValidationError, + }, world::World, }; use alloc::boxed::Box; @@ -351,16 +354,10 @@ impl World { initialized = true; } - let result = system - .validate_param(self) - .map_err(|err| RegisteredSystemError::InvalidParams { system: id, err }) - .map(|()| { - // Wait to run the commands until the system is available again. - // This is needed so the systems can recursively run themselves. - let ret = system.run_without_applying_deferred(input, self); - system.queue_deferred(self.into()); - ret - }); + // Wait to run the commands until the system is available again. + // This is needed so the systems can recursively run themselves. + let result = system.run_without_applying_deferred(input, self); + system.queue_deferred(self.into()); // Return ownership of system trait object (if entity still exists) if let Ok(mut entity) = self.get_entity_mut(id.entity) { @@ -372,7 +369,7 @@ impl World { // Run any commands enqueued by the system self.flush(); - result + Ok(result?) } /// Registers a system or returns its cached [`SystemId`]. @@ -493,14 +490,21 @@ pub enum RegisteredSystemError { #[error("System {0:?} tried to remove itself")] SelfRemove(SystemId), /// System could not be run due to parameters that failed validation. - /// This should not be considered an error if [`field@SystemParamValidationError::skipped`] is `true`. - #[error("System {system:?} did not run due to failed parameter validation: {err}")] - InvalidParams { - /// The identifier of the system that was run. - system: SystemId, - /// The returned parameter validation error. - err: SystemParamValidationError, - }, + /// This is not considered an error. + #[error("System did not run due to failed parameter validation: {0}")] + Skipped(SystemParamValidationError), + /// System returned an error or failed required parameter validation. + #[error("System returned error: {0}")] + Failed(BevyError), +} + +impl From for RegisteredSystemError { + fn from(value: RunSystemError) -> Self { + match value { + RunSystemError::Skipped(err) => Self::Skipped(err), + RunSystemError::Failed(err) => Self::Failed(err), + } + } } impl core::fmt::Debug for RegisteredSystemError { @@ -512,11 +516,8 @@ impl core::fmt::Debug for RegisteredSystemError { Self::SystemNotCached => write!(f, "SystemNotCached"), Self::Recursive(arg0) => f.debug_tuple("Recursive").field(arg0).finish(), Self::SelfRemove(arg0) => f.debug_tuple("SelfRemove").field(arg0).finish(), - Self::InvalidParams { system, err } => f - .debug_struct("InvalidParams") - .field("system", system) - .field("err", err) - .finish(), + Self::Skipped(arg0) => f.debug_tuple("Skipped").field(arg0).finish(), + Self::Failed(arg0) => f.debug_tuple("Failed").field(arg0).finish(), } } } @@ -651,6 +652,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 +765,67 @@ 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] + #[should_panic(expected = "This system always fails")] + fn cached_fallible_system_commands_can_fail() { + use crate::system::command; + fn sys() -> Result { + Err("This system always fails".into()) + } + + let mut world = World::new(); + world.commands().queue(command::run_system_cached(sys)); + world.flush_commands(); } #[test] @@ -787,10 +849,8 @@ mod tests { #[test] fn cached_system_into_same_system_type() { - use crate::error::Result; - struct Foo; - impl IntoSystem<(), Result<()>, ()> for Foo { + impl IntoSystem<(), (), ()> for Foo { type System = ApplyDeferred; fn into_system(_: Self) -> Self::System { ApplyDeferred @@ -798,7 +858,7 @@ mod tests { } struct Bar; - impl IntoSystem<(), Result<()>, ()> for Bar { + impl IntoSystem<(), (), ()> for Bar { type System = ApplyDeferred; fn into_system(_: Self) -> Self::System { ApplyDeferred @@ -865,7 +925,7 @@ mod tests { #[test] fn run_system_invalid_params() { use crate::system::RegisteredSystemError; - use alloc::{format, string::ToString}; + use alloc::string::ToString; struct T; impl Resource for T {} @@ -876,12 +936,9 @@ mod tests { // This fails because `T` has not been added to the world yet. let result = world.run_system(id); - assert!(matches!( - 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"); - assert_eq!(expected, result.unwrap_err().to_string()); + assert!(matches!(result, Err(RegisteredSystemError::Failed { .. }))); + let expected = "System returned error: Parameter `Res` failed validation: Resource does not exist\n"; + assert!(result.unwrap_err().to_string().contains(expected)); } #[test] diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 342ad47849..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. /// @@ -10,23 +14,23 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// Infinite loops are possible, and are not checked for. While looping can be desirable in some contexts /// (for example, an observer that triggers itself multiple times before stopping), following an infinite /// traversal loop without an eventual exit will cause your application to hang. Each implementer of `Traversal` -/// for documenting possible looping behavior, and consumers of those implementations are responsible for +/// is responsible for documenting possible looping behavior, and consumers of those implementations are responsible for /// avoiding infinite loops in their code. /// /// Traversals may be parameterized with additional data. For example, in observer event propagation, the -/// 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/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs deleted file mode 100644 index ea2899c5f9..0000000000 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Internal components used by bevy with a fixed component id. -//! Constants are used to skip [`TypeId`] lookups in hot paths. -use super::*; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; - -/// [`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); - -/// Trigger emitted when a component is inserted onto an entity that does not already have that -/// component. Runs before `OnInsert`. -/// See [`crate::component::ComponentHooks::on_add`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnAdd; - -/// Trigger emitted when a component is inserted, regardless of whether or not the entity already -/// had that component. Runs after `OnAdd`, if it ran. -/// See [`crate::component::ComponentHooks::on_insert`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnInsert; - -/// Trigger emitted when a component is inserted onto an entity that already has that component. -/// Runs before the value is replaced, so you can still access the original component data. -/// See [`crate::component::ComponentHooks::on_replace`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnReplace; - -/// 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::component::ComponentHooks::on_remove`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnRemove; - -/// Trigger emitted for each component on an entity when it is despawned. -/// See [`crate::component::ComponentHooks::on_despawn`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnDespawn; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 02c12fe6a3..64f1fa409f 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -1,11 +1,14 @@ use core::ops::Deref; +use bevy_utils::prelude::DebugName; + use crate::{ archetype::Archetype, change_detection::{MaybeLocation, MutUntyped}, - component::{ComponentId, HookContext, Mutable}, + component::{ComponentId, Mutable}, entity::Entity, - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EntityEvent, Event, EventId, EventKey, Events, WriteBatchIds}, + lifecycle::{HookContext, INSERT, REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, @@ -16,14 +19,14 @@ use crate::{ world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; -use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE}; +use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World}; /// A [`World`] reference that disallows structural ECS changes. /// This includes initializing resources, registering components or spawning entities. /// /// This means that in order to add entities, for example, you will need to use commands instead of the world directly. pub struct DeferredWorld<'w> { - // SAFETY: Implementors must not use this reference to make structural changes + // SAFETY: Implementers must not use this reference to make structural changes world: UnsafeWorldCell<'w>, } @@ -84,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 @@ -93,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. @@ -103,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 @@ -123,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)?; @@ -144,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( @@ -152,12 +164,12 @@ 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, - entity, + REPLACE, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -184,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( @@ -192,12 +204,12 @@ 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, - entity, + INSERT, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -450,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::() ), } } @@ -479,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::() ), } } @@ -495,38 +507,74 @@ impl<'w> DeferredWorld<'w> { unsafe { self.world.get_non_send_resource_mut() } } - /// Sends an [`Event`]. - /// This method returns the [ID](`EventId`) of the sent `event`, - /// or [`None`] if the `event` could not be sent. + /// Writes a [`BufferedEvent`]. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { - self.send_event_batch(core::iter::once(event))?.next() + pub fn write_event(&mut self, event: E) -> Option> { + self.write_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. - /// This method returns the [ID](`EventId`) of the sent `event`, - /// or [`None`] if the `event` could not be sent. + /// Writes a [`BufferedEvent`]. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. #[inline] - pub fn send_event_default(&mut self) -> Option> { - self.send_event(E::default()) + #[deprecated(since = "0.17.0", note = "Use `DeferredWorld::write_event` instead.")] + pub fn send_event(&mut self, event: E) -> Option> { + self.write_event(event) } - /// Sends a batch of [`Event`]s from an iterator. - /// This method returns the [IDs](`EventId`) of the sent `events`, - /// or [`None`] if the `event` could not be sent. + /// Writes the default value of the [`BufferedEvent`] of type `E`. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. #[inline] - pub fn send_event_batch( + pub fn write_event_default(&mut self) -> Option> { + self.write_event(E::default()) + } + + /// Writes the default value of the [`BufferedEvent`] of type `E`. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. + #[inline] + #[deprecated( + since = "0.17.0", + note = "Use `DeferredWorld::write_event_default` instead." + )] + pub fn send_event_default(&mut self) -> Option> { + self.write_event_default::() + } + + /// Writes a batch of [`BufferedEvent`]s from an iterator. + /// This method returns the [IDs](`EventId`) of the written `events`, + /// or [`None`] if the `event` could not be written. + #[inline] + pub fn write_event_batch( &mut self, events: impl IntoIterator, - ) -> Option> { + ) -> 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; }; - Some(events_resource.send_batch(events)) + Some(events_resource.write_batch(events)) + } + + /// Writes a batch of [`BufferedEvent`]s from an iterator. + /// This method returns the [IDs](`EventId`) of the written `events`, + /// or [`None`] if the `event` could not be written. + #[inline] + #[deprecated( + since = "0.17.0", + note = "Use `DeferredWorld::write_event_batch` instead." + )] + pub fn send_event_batch( + &mut self, + events: impl IntoIterator, + ) -> Option> { + self.write_event_batch(events) } /// Gets a pointer to the resource with the id [`ComponentId`] if it exists. @@ -737,8 +785,8 @@ impl<'w> DeferredWorld<'w> { #[inline] pub(crate) unsafe fn trigger_observers( &mut self, - event: ComponentId, - target: Entity, + event: EventKey, + target: Option, components: impl Iterator + Clone, caller: MaybeLocation, ) { @@ -746,6 +794,7 @@ impl<'w> DeferredWorld<'w> { self.reborrow(), event, target, + target, components, &mut (), &mut false, @@ -760,8 +809,9 @@ impl<'w> DeferredWorld<'w> { #[inline] pub(crate) unsafe fn trigger_observers_with_data( &mut self, - event: ComponentId, - mut target: Entity, + event: EventKey, + current_target: Option, + original_target: Option, components: impl Iterator + Clone, data: &mut E, mut propagate: bool, @@ -769,41 +819,64 @@ impl<'w> DeferredWorld<'w> { ) where T: Traversal, { + Observers::invoke::<_>( + self.reborrow(), + event, + current_target, + original_target, + components.clone(), + data, + &mut propagate, + caller, + ); + let Some(mut current_target) = current_target else { + return; + }; + loop { + if !propagate { + return; + } + if let Some(traverse_to) = self + .get_entity(current_target) + .ok() + .and_then(|entity| entity.get_components::()) + .and_then(|item| T::traverse(item, data)) + { + current_target = traverse_to; + } else { + break; + } Observers::invoke::<_>( self.reborrow(), event, - target, + Some(current_target), + original_target, components.clone(), data, &mut propagate, caller, ); - if !propagate { - break; - } - if let Some(traverse_to) = self - .get_entity(target) - .ok() - .and_then(|entity| entity.get_components::()) - .and_then(|item| T::traverse(item, data)) - { - target = traverse_to; - } else { - break; - } } } - /// 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_fetch.rs b/crates/bevy_ecs/src/world/entity_fetch.rs index 8588131563..4aa8baf9e8 100644 --- a/crates/bevy_ecs/src/world/entity_fetch.rs +++ b/crates/bevy_ecs/src/world/entity_fetch.rs @@ -200,6 +200,7 @@ unsafe impl WorldEntityFetch for Entity { type Mut<'w> = EntityWorldMut<'w>; type DeferredMut<'w> = EntityMut<'w>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -209,6 +210,7 @@ unsafe impl WorldEntityFetch for Entity { Ok(unsafe { EntityRef::new(ecell) }) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -220,9 +222,10 @@ unsafe impl WorldEntityFetch for Entity { // SAFETY: caller ensures that the world cell has mutable access to the entity. let world = unsafe { cell.world_mut() }; // SAFETY: location was fetched from the same world's `Entities`. - Ok(unsafe { EntityWorldMut::new(world, self, location) }) + Ok(unsafe { EntityWorldMut::new(world, self, Some(location)) }) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -242,6 +245,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { type Mut<'w> = [EntityMut<'w>; N]; type DeferredMut<'w> = [EntityMut<'w>; N]; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -249,6 +253,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { <&Self>::fetch_ref(&self, cell) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -256,6 +261,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { <&Self>::fetch_mut(&self, cell) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -273,6 +279,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { type Mut<'w> = [EntityMut<'w>; N]; type DeferredMut<'w> = [EntityMut<'w>; N]; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -290,6 +297,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -316,6 +324,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -335,6 +344,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { type Mut<'w> = Vec>; type DeferredMut<'w> = Vec>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -349,6 +359,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -372,6 +383,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -391,6 +403,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { type Mut<'w> = EntityHashMap>; type DeferredMut<'w> = EntityHashMap>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -404,6 +417,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -417,6 +431,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 64610f8e4e..90438b8d6e 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,5 +1,5 @@ use crate::{ - archetype::{Archetype, ArchetypeId}, + archetype::Archetype, bundle::{ Bundle, BundleEffect, BundleFromComponents, BundleInserter, BundleRemover, DynamicBundle, InsertMode, @@ -10,18 +10,17 @@ use crate::{ StorageType, Tick, }, entity::{ - ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityLocation, + ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, + EntityIdLocation, EntityLocation, OptIn, OptOut, }, - event::Event, + event::EntityEvent, + lifecycle::{DESPAWN, REMOVE, REPLACE}, observer::Observer, - query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, + query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, relationship::RelationshipHookMode, resource::Resource, system::IntoObserverSystem, - world::{ - error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World, - ON_DESPAWN, ON_REMOVE, ON_REPLACE, - }, + world::{error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World}, }; use alloc::vec::Vec; use bevy_platform::collections::{HashMap, HashSet}; @@ -280,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::() } } @@ -456,6 +457,7 @@ impl<'w> EntityMut<'w> { /// - `cell` must have permission to mutate every component of the entity. /// - No accesses to any of the entity's components may exist /// at the same time as the returned [`EntityMut`]. + #[inline] pub(crate) unsafe fn new(cell: UnsafeEntityCell<'w>) -> Self { Self { cell } } @@ -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::() } @@ -1009,6 +1013,7 @@ impl<'w> From> for EntityMut<'w> { } impl<'a> From<&'a mut EntityWorldMut<'_>> for EntityMut<'a> { + #[inline] fn from(entity: &'a mut EntityWorldMut<'_>) -> Self { // SAFETY: `EntityWorldMut` guarantees exclusive access to the entire world. unsafe { EntityMut::new(entity.as_unsafe_entity_cell()) } @@ -1096,7 +1101,7 @@ unsafe impl EntityEquivalent for EntityMut<'_> {} pub struct EntityWorldMut<'w> { world: &'w mut World, entity: Entity, - location: EntityLocation, + location: EntityIdLocation, } impl<'w> EntityWorldMut<'w> { @@ -1116,43 +1121,48 @@ impl<'w> EntityWorldMut<'w> { #[inline(always)] #[track_caller] pub(crate) fn assert_not_despawned(&self) { - if self.location.archetype_id == ArchetypeId::INVALID { - self.panic_despawned(); + if self.location.is_none() { + self.panic_despawned() } } + #[inline(always)] fn as_unsafe_entity_cell_readonly(&self) -> UnsafeEntityCell<'_> { - self.assert_not_despawned(); + let location = self.location(); let last_change_tick = self.world.last_change_tick; let change_tick = self.world.read_change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell_readonly(), self.entity, - self.location, + location, last_change_tick, change_tick, ) } + + #[inline(always)] fn as_unsafe_entity_cell(&mut self) -> UnsafeEntityCell<'_> { - self.assert_not_despawned(); + let location = self.location(); let last_change_tick = self.world.last_change_tick; let change_tick = self.world.change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell(), self.entity, - self.location, + location, last_change_tick, change_tick, ) } + + #[inline(always)] fn into_unsafe_entity_cell(self) -> UnsafeEntityCell<'w> { - self.assert_not_despawned(); + let location = self.location(); let last_change_tick = self.world.last_change_tick; let change_tick = self.world.change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell(), self.entity, - self.location, + location, last_change_tick, change_tick, ) @@ -1168,10 +1178,10 @@ impl<'w> EntityWorldMut<'w> { pub(crate) unsafe fn new( world: &'w mut World, entity: Entity, - location: EntityLocation, + location: Option, ) -> Self { debug_assert!(world.entities().contains(entity)); - debug_assert_eq!(world.entities().get(entity), Some(location)); + debug_assert_eq!(world.entities().get(entity), location); EntityWorldMut { world, @@ -1187,6 +1197,7 @@ impl<'w> EntityWorldMut<'w> { } /// Gets read-only access to all of the entity's components. + #[inline] pub fn as_readonly(&self) -> EntityRef<'_> { EntityRef::from(self) } @@ -1198,6 +1209,7 @@ impl<'w> EntityWorldMut<'w> { } /// Gets non-structural mutable access to all of the entity's components. + #[inline] pub fn as_mutable(&mut self) -> EntityMut<'_> { EntityMut::from(self) } @@ -1216,8 +1228,10 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn location(&self) -> EntityLocation { - self.assert_not_despawned(); - self.location + match self.location { + Some(loc) => loc, + None => self.panic_despawned(), + } } /// Returns the archetype that the current entity belongs to. @@ -1227,8 +1241,8 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn archetype(&self) -> &Archetype { - self.assert_not_despawned(); - &self.world.archetypes[self.location.archetype_id] + let location = self.location(); + &self.world.archetypes[location.archetype_id] } /// Returns `true` if the current entity has a component of type `T`. @@ -1300,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::() } @@ -1311,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::() } @@ -1367,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 @@ -1420,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 @@ -1830,22 +1846,22 @@ impl<'w> EntityWorldMut<'w> { caller: MaybeLocation, relationship_hook_mode: RelationshipHookMode, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let change_tick = self.world.change_tick(); let mut bundle_inserter = - BundleInserter::new::(self.world, self.location.archetype_id, change_tick); + BundleInserter::new::(self.world, location.archetype_id, change_tick); // SAFETY: location matches current entity. `T` matches `bundle_info` let (location, after_effect) = unsafe { bundle_inserter.insert( self.entity, - self.location, + location, bundle, mode, caller, relationship_hook_mode, ) }; - self.location = location; + self.location = Some(location); self.world.flush(); self.update_location(); after_effect.apply(self); @@ -1894,7 +1910,7 @@ impl<'w> EntityWorldMut<'w> { caller: MaybeLocation, relationship_hook_insert_mode: RelationshipHookMode, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let change_tick = self.world.change_tick(); let bundle_id = self.world.bundles.init_component_info( &mut self.world.storages, @@ -1903,23 +1919,19 @@ impl<'w> EntityWorldMut<'w> { ); let storage_type = self.world.bundles.get_storage_unchecked(bundle_id); - let bundle_inserter = BundleInserter::new_with_id( - self.world, - self.location.archetype_id, - bundle_id, - change_tick, - ); + let bundle_inserter = + BundleInserter::new_with_id(self.world, location.archetype_id, bundle_id, change_tick); - self.location = insert_dynamic_bundle( + self.location = Some(insert_dynamic_bundle( bundle_inserter, self.entity, - self.location, + location, Some(component).into_iter(), Some(storage_type).iter().cloned(), mode, caller, relationship_hook_insert_mode, - ); + )); self.world.flush(); self.update_location(); self @@ -1957,7 +1969,7 @@ impl<'w> EntityWorldMut<'w> { iter_components: I, relationship_hook_insert_mode: RelationshipHookMode, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let change_tick = self.world.change_tick(); let bundle_id = self.world.bundles.init_dynamic_info( &mut self.world.storages, @@ -1966,23 +1978,19 @@ impl<'w> EntityWorldMut<'w> { ); let mut storage_types = core::mem::take(self.world.bundles.get_storages_unchecked(bundle_id)); - let bundle_inserter = BundleInserter::new_with_id( - self.world, - self.location.archetype_id, - bundle_id, - change_tick, - ); + let bundle_inserter = + BundleInserter::new_with_id(self.world, location.archetype_id, bundle_id, change_tick); - self.location = insert_dynamic_bundle( + self.location = Some(insert_dynamic_bundle( bundle_inserter, self.entity, - self.location, + location, iter_components, (*storage_types).iter().cloned(), InsertMode::Replace, MaybeLocation::caller(), relationship_hook_insert_mode, - ); + )); *self.world.bundles.get_storages_unchecked(bundle_id) = core::mem::take(&mut storage_types); self.world.flush(); self.update_location(); @@ -2000,13 +2008,12 @@ impl<'w> EntityWorldMut<'w> { #[must_use] #[track_caller] pub fn take(&mut self) -> Option { - self.assert_not_despawned(); + let location = self.location(); let entity = self.entity; - let location = self.location; let mut remover = // SAFETY: The archetype id must be valid since this entity is in it. - unsafe { BundleRemover::new::(self.world, self.location.archetype_id, true) }?; + unsafe { BundleRemover::new::(self.world, location.archetype_id, true) }?; // SAFETY: The passed location has the sane archetype as the remover, since they came from the same location. let (new_location, result) = unsafe { remover.remove( @@ -2041,7 +2048,7 @@ impl<'w> EntityWorldMut<'w> { }, ) }; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); @@ -2062,11 +2069,11 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn remove_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let Some(mut remover) = // SAFETY: The archetype id must be valid since this entity is in it. - (unsafe { BundleRemover::new::(self.world, self.location.archetype_id, false) }) + (unsafe { BundleRemover::new::(self.world, location.archetype_id, false) }) else { return self; }; @@ -2074,14 +2081,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2101,7 +2108,7 @@ impl<'w> EntityWorldMut<'w> { &mut self, caller: MaybeLocation, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let storages = &mut self.world.storages; let bundles = &mut self.world.bundles; // SAFETY: These come from the same world. @@ -2112,7 +2119,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2120,14 +2127,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2147,7 +2154,7 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn retain_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { - self.assert_not_despawned(); + let old_location = self.location(); let archetypes = &mut self.world.archetypes; let storages = &mut self.world.storages; // SAFETY: These come from the same world. @@ -2161,7 +2168,6 @@ impl<'w> EntityWorldMut<'w> { .register_info::(&mut registrator, storages); // SAFETY: `retained_bundle` exists as we just initialized it. let retained_bundle_info = unsafe { self.world.bundles.get_unchecked(retained_bundle) }; - let old_location = self.location; let old_archetype = &mut archetypes[old_location.archetype_id]; // PERF: this could be stored in an Archetype Edge @@ -2176,7 +2182,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, remove_bundle, false) + BundleRemover::new_with_id(self.world, old_location.archetype_id, remove_bundle, false) }) else { return self; }; @@ -2184,14 +2190,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + old_location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2216,7 +2222,7 @@ impl<'w> EntityWorldMut<'w> { component_id: ComponentId, caller: MaybeLocation, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let components = &mut self.world.components; let bundle_id = self.world.bundles.init_component_info( @@ -2227,7 +2233,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2235,14 +2241,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2258,7 +2264,7 @@ impl<'w> EntityWorldMut<'w> { /// entity has been despawned while this `EntityWorldMut` is still alive. #[track_caller] pub fn remove_by_ids(&mut self, component_ids: &[ComponentId]) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let components = &mut self.world.components; let bundle_id = self.world.bundles.init_dynamic_info( @@ -2269,7 +2275,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2277,14 +2283,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, MaybeLocation::caller(), BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2302,7 +2308,7 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn clear_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let component_ids: Vec = self.archetype().components().collect(); let components = &mut self.world.components; @@ -2314,7 +2320,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2322,14 +2328,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2353,9 +2359,9 @@ impl<'w> EntityWorldMut<'w> { } pub(crate) fn despawn_with_caller(self, caller: MaybeLocation) { - self.assert_not_despawned(); + let location = self.location(); let world = self.world; - let archetype = &world.archetypes[self.location.archetype_id]; + let archetype = &world.archetypes[location.archetype_id]; // SAFETY: Archetype cannot be mutably aliased by DeferredWorld let (archetype, mut deferred_world) = unsafe { @@ -2368,8 +2374,8 @@ impl<'w> EntityWorldMut<'w> { unsafe { if archetype.has_despawn_observer() { deferred_world.trigger_observers( - ON_DESPAWN, - self.entity, + DESPAWN, + Some(self.entity), archetype.components(), caller, ); @@ -2382,8 +2388,8 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - self.entity, + REPLACE, + Some(self.entity), archetype.components(), caller, ); @@ -2397,8 +2403,8 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, - self.entity, + REMOVE, + Some(self.entity), archetype.components(), caller, ); @@ -2412,7 +2418,7 @@ impl<'w> EntityWorldMut<'w> { } for component_id in archetype.components() { - world.removed_components.send(component_id, self.entity); + world.removed_components.write(component_id, self.entity); } // Observers and on_remove hooks may reserve new entities, which @@ -2422,30 +2428,32 @@ impl<'w> EntityWorldMut<'w> { let location = world .entities .free(self.entity) + .flatten() .expect("entity should exist at this point."); let table_row; let moved_entity; let change_tick = world.change_tick(); { - let archetype = &mut world.archetypes[self.location.archetype_id]; + let archetype = &mut world.archetypes[location.archetype_id]; let remove_result = archetype.swap_remove(location.archetype_row); if let Some(swapped_entity) = remove_result.swapped_entity { let swapped_location = world.entities.get(swapped_entity).unwrap(); // SAFETY: swapped_entity is valid and the swapped entity's components are // moved to the new location immediately after. unsafe { - world.entities.set_spawn_despawn( + world.entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, - caller, - change_tick, + }), ); + world + .entities + .mark_spawn_despawn(swapped_entity.index(), caller, change_tick); } } table_row = remove_result.table_row; @@ -2466,17 +2474,18 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: `moved_entity` is valid and the provided `EntityLocation` accurately reflects // the current location of the entity and its component data. unsafe { - world.entities.set_spawn_despawn( + world.entities.set( moved_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: moved_location.archetype_id, archetype_row: moved_location.archetype_row, table_id: moved_location.table_id, table_row, - }, - caller, - change_tick, + }), ); + world + .entities + .mark_spawn_despawn(moved_entity.index(), caller, change_tick); } world.archetypes[moved_location.archetype_id] .set_entity_table_row(moved_location.archetype_row, table_row); @@ -2564,11 +2573,7 @@ impl<'w> EntityWorldMut<'w> { /// This is *only* required when using the unsafe function [`EntityWorldMut::world_mut`], /// which enables the location to change. pub fn update_location(&mut self) { - self.location = self - .world - .entities() - .get(self.entity) - .unwrap_or(EntityLocation::INVALID); + self.location = self.world.entities().get(self.entity); } /// Returns if the entity has been despawned. @@ -2580,7 +2585,7 @@ impl<'w> EntityWorldMut<'w> { /// to avoid panicking when calling further methods. #[inline] pub fn is_despawned(&self) -> bool { - self.location.archetype_id == ArchetypeId::INVALID + self.location.is_none() } /// Gets an Entry into the world for this entity and component for in-place manipulation. @@ -2608,14 +2613,14 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn entry<'a, T: Component>(&'a mut self) -> Entry<'w, 'a, T> { + pub fn entry<'a, T: Component>(&'a mut self) -> ComponentEntry<'w, 'a, T> { if self.contains::() { - Entry::Occupied(OccupiedEntry { + ComponentEntry::Occupied(OccupiedComponentEntry { entity_world: self, _marker: PhantomData, }) } else { - Entry::Vacant(VacantEntry { + ComponentEntry::Vacant(VacantComponentEntry { entity_world: self, _marker: PhantomData, }) @@ -2627,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(); @@ -2644,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, @@ -2667,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)] @@ -2680,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); @@ -2715,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); @@ -2781,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); @@ -2804,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); @@ -2858,14 +2964,14 @@ impl<'w> EntityWorldMut<'w> { /// This `enum` can only be constructed from the [`entry`] method on [`EntityWorldMut`]. /// /// [`entry`]: EntityWorldMut::entry -pub enum Entry<'w, 'a, T: Component> { +pub enum ComponentEntry<'w, 'a, T: Component> { /// An occupied entry. - Occupied(OccupiedEntry<'w, 'a, T>), + Occupied(OccupiedComponentEntry<'w, 'a, T>), /// A vacant entry. - Vacant(VacantEntry<'w, 'a, T>), + Vacant(VacantComponentEntry<'w, 'a, T>), } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { /// Provides in-place mutable access to an occupied entry. /// /// # Examples @@ -2884,17 +2990,17 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { #[inline] pub fn and_modify)>(self, f: F) -> Self { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { f(entry.get_mut()); - Entry::Occupied(entry) + ComponentEntry::Occupied(entry) } - Entry::Vacant(entry) => Entry::Vacant(entry), + ComponentEntry::Vacant(entry) => ComponentEntry::Vacant(entry), } } } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { - /// Replaces the component of the entry, and returns an [`OccupiedEntry`]. +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { + /// Replaces the component of the entry, and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// @@ -2913,13 +3019,13 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(entry.get(), &Comp(2)); /// ``` #[inline] - pub fn insert_entry(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert_entry(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { entry.insert(component); entry } - Entry::Vacant(entry) => entry.insert(component), + ComponentEntry::Vacant(entry) => entry.insert(component), } } @@ -2945,10 +3051,10 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 8); /// ``` #[inline] - pub fn or_insert(self, default: T) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert(self, default: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default), } } @@ -2969,15 +3075,15 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 4); /// ``` #[inline] - pub fn or_insert_with T>(self, default: F) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert_with T>(self, default: F) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default()), } } } -impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component + Default> ComponentEntry<'w, 'a, T> { /// Ensures the entry has this component by inserting the default value if empty, and /// returns a mutable reference to this component in the entry. /// @@ -2995,42 +3101,42 @@ impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 0); /// ``` #[inline] - pub fn or_default(self) -> OccupiedEntry<'w, 'a, T> { + pub fn or_default(self) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(Default::default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(Default::default()), } } } -/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. +/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`OccupiedComponentEntry`] enum. /// /// The contained entity must have the component type parameter if we have this struct. -pub struct OccupiedEntry<'w, 'a, T: Component> { +pub struct OccupiedComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a reference to the component in the entry. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.get().0, 5); /// } /// ``` #[inline] pub fn get(&self) -> &T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get::().unwrap() } @@ -3039,14 +3145,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.insert(Comp(10)); /// } /// @@ -3062,14 +3168,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.take(), Comp(5)); /// } /// @@ -3077,30 +3183,30 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn take(self) -> T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.take().unwrap() } } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a mutable reference to the component in the entry. /// - /// If you need a reference to the `OccupiedEntry` which may outlive the destruction of - /// the `Entry` value, see [`into_mut`]. + /// If you need a reference to the [`OccupiedComponentEntry`] which may outlive the destruction of + /// the [`OccupiedComponentEntry`] value, see [`into_mut`]. /// /// [`into_mut`]: Self::into_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.get_mut().0 += 10; /// assert_eq!(o.get().0, 15); /// @@ -3112,28 +3218,28 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn get_mut(&mut self) -> Mut<'_, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut::().unwrap() } - /// Converts the `OccupiedEntry` into a mutable reference to the value in the entry with + /// Converts the [`OccupiedComponentEntry`] into a mutable reference to the value in the entry with /// a lifetime bound to the `EntityWorldMut`. /// - /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// If you need multiple references to the [`OccupiedComponentEntry`], see [`get_mut`]. /// /// [`get_mut`]: Self::get_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// o.into_mut().0 += 10; /// } /// @@ -3141,40 +3247,40 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn into_mut(self) -> Mut<'a, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut().unwrap() } } -/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. -pub struct VacantEntry<'w, 'a, T: Component> { +/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`ComponentEntry`] enum. +pub struct VacantComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { - /// Inserts the component into the `VacantEntry` and returns an `OccupiedEntry`. +impl<'w, 'a, T: Component> VacantComponentEntry<'w, 'a, T> { + /// Inserts the component into the [`VacantComponentEntry`] and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn_empty(); /// - /// if let Entry::Vacant(v) = entity.entry::() { + /// if let ComponentEntry::Vacant(v) = entity.entry::() { /// v.insert(Comp(10)); /// } /// /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 10); /// ``` #[inline] - pub fn insert(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { self.entity_world.insert(component); - OccupiedEntry { + OccupiedComponentEntry { entity_world: self.entity_world, _marker: PhantomData, } @@ -3185,7 +3291,7 @@ impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { /// /// To define the access when used as a [`QueryData`](crate::query::QueryData), /// use a [`QueryBuilder`](crate::query::QueryBuilder) or [`QueryParamBuilder`](crate::system::QueryParamBuilder). -/// The `FilteredEntityRef` must be the entire `QueryData`, and not nested inside a tuple with other data. +/// The [`FilteredEntityRef`] must be the entire [`QueryData`](crate::query::QueryData), and not nested inside a tuple with other data. /// /// ``` /// # use bevy_ecs::{prelude::*, world::FilteredEntityRef}; @@ -3287,7 +3393,11 @@ impl<'w> FilteredEntityRef<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get(&self) -> Option<&'w T> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3301,7 +3411,11 @@ impl<'w> FilteredEntityRef<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get_ref(&self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3313,7 +3427,11 @@ impl<'w> FilteredEntityRef<'w> { /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3645,7 +3763,11 @@ impl<'w> FilteredEntityMut<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get_mut>(&mut self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_write(id) // SAFETY: We have write access @@ -3673,7 +3795,11 @@ impl<'w> FilteredEntityMut<'w> { /// - `T` must be a mutable component #[inline] pub unsafe fn into_mut_assume_mutable(self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_write(id) // SAFETY: @@ -3903,7 +4029,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -3923,7 +4049,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -4003,7 +4129,11 @@ where /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - let component_id = self.entity.world().components().get_id(TypeId::of::())?; + let component_id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; let components = self.entity.world().components(); (!bundle_contains_component::(components, component_id)) .then(|| { @@ -4172,7 +4302,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -4731,7 +4861,8 @@ mod tests { use core::panic::AssertUnwindSafe; use std::sync::OnceLock; - use crate::component::{HookContext, Tick}; + use crate::component::Tick; + use crate::lifecycle::HookContext; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, @@ -4755,7 +4886,7 @@ mod tests { let entity = world.spawn(TestComponent(42)).id(); let component_id = world .components() - .get_id(core::any::TypeId::of::()) + .get_valid_id(core::any::TypeId::of::()) .unwrap(); let entity = world.entity(entity); @@ -4772,7 +4903,7 @@ mod tests { let entity = world.spawn(TestComponent(42)).id(); let component_id = world .components() - .get_id(core::any::TypeId::of::()) + .get_valid_id(core::any::TypeId::of::()) .unwrap(); let mut entity_mut = world.entity_mut(entity); @@ -5715,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] @@ -5723,7 +5854,7 @@ mod tests { let mut world = World::new(); let entity = world .spawn_empty() - .observe(|trigger: Trigger, mut commands: Commands| { + .observe(|trigger: On, mut commands: Commands| { commands.entity(trigger.target()).insert(TestComponent(0)); }) .id(); @@ -5733,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)); } @@ -5742,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()).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)); @@ -5764,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); @@ -5836,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"); } @@ -5887,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"); } @@ -5972,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)] @@ -5987,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))); @@ -6175,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/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index a9fac308fa..ed3672bef9 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -157,7 +157,7 @@ impl<'w, 's> FilteredResources<'w, 's> { let component_id = self .world .components() - .resource_id::() + .valid_resource_id::() .ok_or(ResourceFetchError::NotRegistered)?; if !self.access.has_resource_read(component_id) { return Err(ResourceFetchError::NoResourceAccess(component_id)); @@ -474,7 +474,7 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { let component_id = self .world .components() - .resource_id::() + .valid_resource_id::() .ok_or(ResourceFetchError::NotRegistered)?; // SAFETY: THe caller ensures that there are no conflicting borrows. unsafe { self.get_mut_by_id_unchecked(component_id) } 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 87340551af..e77b348c96 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,7 +1,6 @@ //! Defines the [`World`] and APIs for accessing it directly. pub(crate) mod command_queue; -mod component_constants; mod deferred_world; mod entity_fetch; mod entity_ref; @@ -18,37 +17,44 @@ pub use crate::{ change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, world::command_queue::CommandQueue, }; +use crate::{ + error::{DefaultErrorHandler, ErrorHandler}, + event::BufferedEvent, + lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, + prelude::{Add, Despawn, Insert, Remove, Replace}, +}; pub use bevy_ecs_macros::FromWorld; -pub use component_constants::*; +use bevy_utils::prelude::DebugName; pub use deferred_world::DeferredWorld; pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_ref::{ - DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, - Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, TryFromFilteredError, VacantEntry, + ComponentEntry, DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, + EntityWorldMut, FilteredEntityMut, FilteredEntityRef, OccupiedComponentEntry, + TryFromFilteredError, VacantComponentEntry, }; pub use filtered_resource::*; pub use identifier::WorldId; pub use spawn_batch::*; use crate::{ - archetype::{ArchetypeId, ArchetypeRow, Archetypes}, + archetype::{ArchetypeId, Archetypes}, bundle::{ Bundle, BundleEffect, BundleInfo, BundleInserter, BundleSpawner, Bundles, InsertMode, NoBundleEffect, }, change_detection::{MaybeLocation, MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentIds, ComponentInfo, + CheckChangeTicks, Component, ComponentDescriptor, ComponentId, ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, - entity::{Entities, Entity, EntityDoesNotExistError, EntityLocation}, + entity::{Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, - event::{Event, EventId, Events, SendBatchIds}, + event::{Event, EventId, Events, WriteBatchIds}, + lifecycle::RemovedComponentEvents, observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, - removal_detection::RemovedComponentEvents, resource::Resource, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, @@ -146,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_event_key(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_event_key(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_event_key(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_event_key(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_event_key(self); + assert_eq!(DESPAWN, on_despawn); // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); @@ -210,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 { @@ -256,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] @@ -284,6 +304,7 @@ impl World { /// Returns a mutable reference to the [`ComponentHooks`] for a [`Component`] type. /// /// Will panic if `T` exists in any archetypes. + #[must_use] pub fn register_component_hooks(&mut self) -> &mut ComponentHooks { let index = self.register_component::(); assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(index)), "Components hooks cannot be modified if the component already exists in an archetype, use register_component if {} may already be in use", core::any::type_name::()); @@ -298,7 +319,7 @@ impl World { &mut self, id: ComponentId, ) -> Option<&mut ComponentHooks> { - assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(id)), "Components hooks cannot be modified if the component already exists in an archetype, use register_component if the component with id {:?} may already be in use", id); + assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(id)), "Components hooks cannot be modified if the component already exists in an archetype, use register_component if the component with id {id:?} may already be in use"); self.components.get_hooks_mut(id) } @@ -530,7 +551,7 @@ impl World { /// Retrieves the [required components](RequiredComponents) for the given component type, if it exists. pub fn get_required_components(&self) -> Option<&RequiredComponents> { - let id = self.components().component_id::()?; + let id = self.components().valid_component_id::()?; let component_info = self.components().get_info(id)?; Some(component_info.required_components()) } @@ -959,18 +980,8 @@ impl World { pub fn iter_entities(&self) -> impl Iterator> + '_ { self.archetypes.iter().flat_map(|archetype| { archetype - .entities() - .iter() - .enumerate() - .map(|(archetype_row, archetype_entity)| { - let entity = archetype_entity.id(); - let location = EntityLocation { - archetype_id: archetype.id(), - archetype_row: ArchetypeRow::new(archetype_row), - table_id: archetype.table_id(), - table_row: archetype_entity.table_row(), - }; - + .entities_with_location() + .map(|(entity, location)| { // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored. let cell = UnsafeEntityCell::new( self.as_unsafe_world_cell_readonly(), @@ -992,18 +1003,8 @@ impl World { let world_cell = self.as_unsafe_world_cell(); world_cell.archetypes().iter().flat_map(move |archetype| { archetype - .entities() - .iter() - .enumerate() - .map(move |(archetype_row, archetype_entity)| { - let entity = archetype_entity.id(); - let location = EntityLocation { - archetype_id: archetype.id(), - archetype_row: ArchetypeRow::new(archetype_row), - table_id: archetype.table_id(), - table_row: archetype_entity.table_row(), - }; - + .entities_with_location() + .map(move |(entity, location)| { // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored. let cell = UnsafeEntityCell::new( world_cell, @@ -1173,16 +1174,15 @@ impl World { let entity = self.entities.alloc(); let mut bundle_spawner = BundleSpawner::new::(self, change_tick); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent - let (mut entity_location, after_effect) = + let (entity_location, after_effect) = unsafe { bundle_spawner.spawn_non_existent(entity, bundle, caller) }; + let mut entity_location = Some(entity_location); + // SAFETY: command_queue is not referenced anywhere else if !unsafe { self.command_queue.is_empty() } { self.flush(); - entity_location = self - .entities() - .get(entity) - .unwrap_or(EntityLocation::INVALID); + entity_location = self.entities().get(entity); } // SAFETY: entity and location are valid, as they were just created above @@ -1205,10 +1205,11 @@ impl World { // empty let location = unsafe { archetype.allocate(entity, table_row) }; let change_tick = self.change_tick(); + self.entities.set(entity.index(), Some(location)); self.entities - .set_spawn_despawn(entity.index(), location, caller, change_tick); + .mark_spawn_despawn(entity.index(), caller, change_tick); - EntityWorldMut::new(self, entity, location) + EntityWorldMut::new(self, entity, Some(location)) } /// Spawns a batch of entities with the same component [`Bundle`] type. Takes a given @@ -1289,7 +1290,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 @@ -1319,6 +1320,7 @@ impl World { /// # assert_eq!(world.get::(entity), Some(&Foo(true))); /// ``` #[inline] + #[track_caller] pub fn modify_component( &mut self, entity: Entity, @@ -1326,7 +1328,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) @@ -1335,7 +1341,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 @@ -1347,6 +1353,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, @@ -1355,7 +1362,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) @@ -1463,7 +1475,7 @@ impl World { /// assert!(!transform.is_changed()); /// ``` /// - /// [`RemovedComponents`]: crate::removal_detection::RemovedComponents + /// [`RemovedComponents`]: crate::lifecycle::RemovedComponents pub fn clear_trackers(&mut self) { self.removed_components.update(); self.last_change_tick = self.increment_change_tick(); @@ -1642,7 +1654,7 @@ impl World { /// since the last call to [`World::clear_trackers`]. pub fn removed(&self) -> impl Iterator + '_ { self.components - .get_id(TypeId::of::()) + .get_valid_id(TypeId::of::()) .map(|component_id| self.removed_with_id(component_id)) .into_iter() .flatten() @@ -1791,7 +1803,7 @@ impl World { /// Removes the resource of a given type and returns it, if it exists. Otherwise returns `None`. #[inline] pub fn remove_resource(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, _, _) = self.storages.resources.get_mut(component_id)?.remove()?; // SAFETY: `component_id` was gotten via looking up the `R` type unsafe { Some(ptr.read::()) } @@ -1810,7 +1822,7 @@ impl World { /// thread than where the value was inserted from. #[inline] pub fn remove_non_send_resource(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, _, _) = self .storages .non_send_resources @@ -1824,7 +1836,7 @@ impl World { #[inline] pub fn contains_resource(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.storages.resources.get(component_id)) .is_some_and(ResourceData::is_present) } @@ -1842,7 +1854,7 @@ impl World { #[inline] pub fn contains_non_send(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.storages.non_send_resources.get(component_id)) .is_some_and(ResourceData::is_present) } @@ -1865,7 +1877,7 @@ impl World { /// was called. pub fn is_resource_added(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .is_some_and(|component_id| self.is_resource_added_by_id(component_id)) } @@ -1896,7 +1908,7 @@ impl World { /// was called. pub fn is_resource_changed(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .is_some_and(|component_id| self.is_resource_changed_by_id(component_id)) } @@ -1921,7 +1933,7 @@ impl World { /// Retrieves the change ticks for the given resource. pub fn get_resource_change_ticks(&self) -> Option { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.get_resource_change_ticks_by_id(component_id)) } @@ -1957,7 +1969,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::() ), } } @@ -1981,7 +1993,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::() ), } } @@ -2005,7 +2017,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::() ), } } @@ -2169,7 +2181,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::() ), } } @@ -2191,7 +2203,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::() ), } } @@ -2358,11 +2370,11 @@ impl World { ) }; } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevyengine.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://bevyengine.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)); } } } @@ -2526,7 +2538,7 @@ impl World { Ok(()) } else { Err(TryInsertBatchError { - bundle_type: core::any::type_name::(), + bundle_type: DebugName::type_name::(), entities: invalid_entities, }) } @@ -2560,7 +2572,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, @@ -2577,7 +2589,7 @@ impl World { let last_change_tick = self.last_change_tick(); let change_tick = self.change_tick(); - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, mut ticks, mut caller) = self .storages .resources @@ -2600,7 +2612,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 @@ -2614,38 +2626,68 @@ impl World { Some(result) } - /// Sends an [`Event`]. - /// This method returns the [ID](`EventId`) of the sent `event`, - /// or [`None`] if the `event` could not be sent. + /// Writes a [`BufferedEvent`]. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { - self.send_event_batch(core::iter::once(event))?.next() + pub fn write_event(&mut self, event: E) -> Option> { + self.write_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. - /// This method returns the [ID](`EventId`) of the sent `event`, - /// or [`None`] if the `event` could not be sent. + /// Writes a [`BufferedEvent`]. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. #[inline] - pub fn send_event_default(&mut self) -> Option> { - self.send_event(E::default()) + #[deprecated(since = "0.17.0", note = "Use `World::write_event` instead.")] + pub fn send_event(&mut self, event: E) -> Option> { + self.write_event(event) } - /// Sends a batch of [`Event`]s from an iterator. - /// This method returns the [IDs](`EventId`) of the sent `events`, - /// or [`None`] if the `event` could not be sent. + /// Writes the default value of the [`BufferedEvent`] of type `E`. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. #[inline] - pub fn send_event_batch( + pub fn write_event_default(&mut self) -> Option> { + self.write_event(E::default()) + } + + /// Writes the default value of the [`BufferedEvent`] of type `E`. + /// This method returns the [ID](`EventId`) of the written `event`, + /// or [`None`] if the `event` could not be written. + #[inline] + #[deprecated(since = "0.17.0", note = "Use `World::write_event_default` instead.")] + pub fn send_event_default(&mut self) -> Option> { + self.write_event_default::() + } + + /// Writes a batch of [`BufferedEvent`]s from an iterator. + /// This method returns the [IDs](`EventId`) of the written `events`, + /// or [`None`] if the `event` could not be written. + #[inline] + pub fn write_event_batch( &mut self, events: impl IntoIterator, - ) -> Option> { + ) -> 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; }; - Some(events_resource.send_batch(events)) + Some(events_resource.write_batch(events)) + } + + /// Writes a batch of [`BufferedEvent`]s from an iterator. + /// This method returns the [IDs](`EventId`) of the written `events`, + /// or [`None`] if the `event` could not be written. + #[inline] + #[deprecated(since = "0.17.0", note = "Use `World::write_event_batch` instead.")] + pub fn send_event_batch( + &mut self, + events: impl IntoIterator, + ) -> Option> { + self.write_event_batch(events) } /// Inserts a new resource with the given `value`. Will replace the value if it already existed. @@ -2709,12 +2751,9 @@ impl World { component_id: ComponentId, ) -> &mut ResourceData { self.flush_components(); - let archetypes = &mut self.archetypes; self.storages .resources - .initialize_with(component_id, &self.components, || { - archetypes.new_archetype_component_id() - }) + .initialize_with(component_id, &self.components) } /// # Panics @@ -2725,28 +2764,32 @@ impl World { component_id: ComponentId, ) -> &mut ResourceData { self.flush_components(); - let archetypes = &mut self.archetypes; self.storages .non_send_resources - .initialize_with(component_id, &self.components, || { - archetypes.new_archetype_component_id() - }) + .initialize_with(component_id, &self.components) } /// Empties queued entities and adds them to the empty [`Archetype`](crate::archetype::Archetype). /// This should be called before doing operations that might operate on queued entities, /// such as inserting a [`Component`]. + #[track_caller] pub(crate) fn flush_entities(&mut self) { + let by = MaybeLocation::caller(); + let at = self.change_tick(); let empty_archetype = self.archetypes.empty_mut(); let table = &mut self.storages.tables[empty_archetype.table_id()]; // PERF: consider pre-allocating space for flushed entities // SAFETY: entity is set to a valid location unsafe { - self.entities.flush(|entity, location| { - // SAFETY: no components are allocated by archetype.allocate() because the archetype - // is empty - *location = empty_archetype.allocate(entity, table.allocate(entity)); - }); + self.entities.flush( + |entity, location| { + // SAFETY: no components are allocated by archetype.allocate() because the archetype + // is empty + *location = Some(empty_archetype.allocate(entity, table.allocate(entity))); + }, + by, + at, + ); } } @@ -2781,6 +2824,7 @@ impl World { /// /// Queued entities will be spawned, and then commands will be applied. #[inline] + #[track_caller] pub fn flush(&mut self) { self.flush_entities(); self.flush_components(); @@ -2951,17 +2995,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, @@ -2971,17 +3019,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(check); + self.flush(); + self.last_check_tick = change_tick; + + Some(check) } /// Runs both [`clear_entities`](Self::clear_entities) and [`clear_resources`](Self::clear_resources), @@ -3046,6 +3099,16 @@ impl World { // SAFETY: We just initialized the bundle so its id should definitely be valid. unsafe { self.bundles.get(id).debug_checked_unwrap() } } + + /// Convenience method for accessing the world's default error handler, + /// which can be overwritten with [`DefaultErrorHandler`]. + #[inline] + pub fn default_error_handler(&self) -> ErrorHandler { + self.get_resource::() + .copied() + .unwrap_or_default() + .0 + } } impl World { @@ -3391,11 +3454,18 @@ impl World { // Schedule-related methods impl World { - /// Adds the specified [`Schedule`] to the world. The schedule can later be run + /// Adds the specified [`Schedule`] to the world. + /// If a schedule already exists with the same [label](Schedule::label), it will be replaced. + /// + /// The schedule can later be run /// by calling [`.run_schedule(label)`](Self::run_schedule) or by directly /// accessing the [`Schedules`] resource. /// /// The `Schedules` resource will be initialized if it does not already exist. + /// + /// An alternative to this is to call [`Schedules::add_systems()`] with some + /// [`ScheduleLabel`] and let the schedule for that label be created if it + /// does not already exist. pub fn add_schedule(&mut self, schedule: Schedule) { let mut schedules = self.get_resource_or_init::(); schedules.insert(schedule); @@ -3501,6 +3571,7 @@ impl World { /// and system state is cached. /// /// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead. + /// This avoids the need to create a unique [`ScheduleLabel`]. /// /// # Panics /// @@ -3613,6 +3684,7 @@ mod tests { }; use bevy_ecs_macros::Component; use bevy_platform::collections::{HashMap, HashSet}; + use bevy_utils::prelude::DebugName; use core::{ any::TypeId, panic, @@ -3749,7 +3821,7 @@ mod tests { world.insert_resource(TestResource(42)); let component_id = world .components() - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .unwrap(); let resource = world.get_resource_by_id(component_id).unwrap(); @@ -3765,7 +3837,7 @@ mod tests { world.insert_resource(TestResource(42)); let component_id = world .components() - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .unwrap(); { @@ -3796,12 +3868,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 }, @@ -3824,14 +3896,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 fdd8b28142..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}; @@ -70,17 +70,14 @@ impl World { entity: Entity, type_id: TypeId, ) -> Result<&dyn Reflect, GetComponentReflectError> { - let Some(component_id) = self.components().get_id(type_id) else { + let Some(component_id) = self.components().get_valid_id(type_id) else { return Err(GetComponentReflectError::NoCorrespondingComponentId( type_id, )); }; 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, @@ -158,7 +155,7 @@ impl World { )); }; - let Some(component_id) = self.components().get_id(type_id) else { + let Some(component_id) = self.components().get_valid_id(type_id) else { return Err(GetComponentReflectError::NoCorrespondingComponentId( type_id, )); @@ -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 ea5f21c22e..38d4333843 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -7,10 +7,11 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells}, entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation}, + error::{DefaultErrorHandler, ErrorHandler}, + lifecycle::RemovedComponentEvents, observer::Observers, prelude::Component, - query::{DebugCheckedUnwrap, ReadOnlyQueryData}, - removal_detection::RemovedComponentEvents, + query::{DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -35,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`](crate::system::System::component_access). +/// 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. /// @@ -198,13 +199,13 @@ impl<'w> UnsafeWorldCell<'w> { /// /// # Safety /// - must have permission to access the whole world immutably - /// - there must be no live exclusive borrows on world data + /// - there must be no live exclusive borrows of world data /// - there must be no live exclusive borrow of world #[inline] pub unsafe fn world(self) -> &'w World { // SAFETY: // - caller ensures there is no `&mut World` this makes it okay to make a `&World` - // - caller ensures there is no mutable borrows of world data, this means the caller cannot + // - caller ensures there are no mutable borrows of world data, this means the caller cannot // misuse the returned `&World` unsafe { self.unsafe_world() } } @@ -233,7 +234,7 @@ impl<'w> UnsafeWorldCell<'w> { /// /// # Safety /// - must not be used in a way that would conflict with any - /// live exclusive borrows on world data + /// live exclusive borrows of world data #[inline] unsafe fn unsafe_world(self) -> &'w World { // SAFETY: @@ -395,12 +396,12 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a reference to the resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_resource(self) -> Option<&'w R> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures `self` has permission to access the resource // caller also ensure that no mutable reference to the resource exists unsafe { @@ -413,15 +414,15 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a reference including change detection to the resource of the given type if it exists. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_resource_ref(self) -> Option> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures `self` has permission to access the resource - // caller also ensure that no mutable reference to the resource exists + // caller also ensures that no mutable reference to the resource exists let (ptr, ticks, caller) = unsafe { self.get_resource_with_ticks(component_id)? }; // SAFETY: `component_id` was obtained from the type ID of `R` @@ -449,7 +450,7 @@ impl<'w> UnsafeWorldCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] @@ -465,12 +466,12 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a reference to the non-send resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_non_send_resource(self) -> Option<&'w R> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures that `self` has permission to access `R` // caller ensures that no mutable reference exists to `R` unsafe { @@ -491,7 +492,7 @@ impl<'w> UnsafeWorldCell<'w> { /// This function will panic if it isn't called from the same thread that the resource was inserted from. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] @@ -507,13 +508,13 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a mutable reference to the resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] pub unsafe fn get_resource_mut(self) -> Option> { self.assert_allows_mutable_access(); - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: // - caller ensures `self` has permission to access the resource mutably // - caller ensures no other references to the resource exist @@ -532,7 +533,7 @@ impl<'w> UnsafeWorldCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] @@ -571,13 +572,13 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a mutable reference to the non-send resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] pub unsafe fn get_non_send_resource_mut(self) -> Option> { self.assert_allows_mutable_access(); - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: // - caller ensures that `self` has permission to access the resource // - caller ensures that the resource is unaliased @@ -599,7 +600,7 @@ impl<'w> UnsafeWorldCell<'w> { /// This function will panic if it isn't called from the same thread that the resource was inserted from. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] @@ -633,7 +634,7 @@ impl<'w> UnsafeWorldCell<'w> { // Shorthand helper function for getting the data and change ticks for a resource. /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no mutable references to the resource exist at the same time #[inline] @@ -660,7 +661,7 @@ impl<'w> UnsafeWorldCell<'w> { /// This function will panic if it isn't called from the same thread that the resource was inserted from. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no mutable references to the resource exist at the same time #[inline] @@ -684,7 +685,7 @@ impl<'w> UnsafeWorldCell<'w> { // Returns a mutable reference to the underlying world's [`CommandQueue`]. /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the queue mutably /// - no mutable references to the queue exist at the same time pub(crate) unsafe fn get_raw_command_queue(self) -> RawCommandQueue { @@ -696,7 +697,7 @@ impl<'w> UnsafeWorldCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that there are no outstanding + /// It is the caller's responsibility to ensure that there are no outstanding /// references to `last_trigger_id`. pub(crate) unsafe fn increment_trigger_id(self) { self.assert_allows_mutable_access(); @@ -705,6 +706,18 @@ impl<'w> UnsafeWorldCell<'w> { (*self.ptr).last_trigger_id = (*self.ptr).last_trigger_id.wrapping_add(1); } } + + /// Convenience method for accessing the world's default error handler, + /// + /// # Safety + /// Must have read access to [`DefaultErrorHandler`]. + #[inline] + pub unsafe fn default_error_handler(&self) -> ErrorHandler { + self.get_resource::() + .copied() + .unwrap_or_default() + .0 + } } impl Debug for UnsafeWorldCell<'_> { @@ -714,7 +727,7 @@ impl Debug for UnsafeWorldCell<'_> { } } -/// A interior-mutable reference to a particular [`Entity`] and all of its components +/// An interior-mutable reference to a particular [`Entity`] and all of its components #[derive(Copy, Clone)] pub struct UnsafeEntityCell<'w> { world: UnsafeWorldCell<'w>, @@ -808,12 +821,12 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get(self) -> Option<&'w T> { - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct (T component_id + T::STORAGE_TYPE) // - `location` is valid @@ -832,14 +845,14 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get_ref(self) -> Option> { let last_change_tick = self.last_run; let change_tick = self.this_run; - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct (T component_id + T::STORAGE_TYPE) @@ -866,12 +879,12 @@ impl<'w> UnsafeEntityCell<'w> { /// detection in custom runtimes. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get_change_ticks(self) -> Option { - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - entity location is valid @@ -895,7 +908,7 @@ impl<'w> UnsafeEntityCell<'w> { /// compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] @@ -920,7 +933,7 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time #[inline] @@ -932,7 +945,7 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time /// - the component `T` is mutable @@ -943,7 +956,7 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time /// - The component `T` is mutable @@ -955,7 +968,7 @@ impl<'w> UnsafeEntityCell<'w> { ) -> Option> { self.world.assert_allows_mutable_access(); - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct @@ -982,10 +995,12 @@ impl<'w> UnsafeEntityCell<'w> { /// or `None` if the entity does not have the components required by the query `Q`. /// /// # Safety - /// It is the callers responsibility to ensure that + /// 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(); @@ -1015,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 } @@ -1031,7 +1047,7 @@ impl<'w> UnsafeEntityCell<'w> { /// which is only valid while the `'w` borrow of the lifetime is active. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] @@ -1056,7 +1072,7 @@ impl<'w> UnsafeEntityCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time #[inline] @@ -1104,7 +1120,7 @@ impl<'w> UnsafeEntityCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time /// - the component `T` is mutable diff --git a/crates/bevy_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index b2f1b92d82..9e05bc7a85 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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_encase_derive/src/lib.rs b/crates/bevy_encase_derive/src/lib.rs index 15fbdca6a8..d882cb5cae 100644 --- a/crates/bevy_encase_derive/src/lib.rs +++ b/crates/bevy_encase_derive/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] use bevy_macro_utils::BevyManifest; diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml new file mode 100644 index 0000000000..07d883704a --- /dev/null +++ b/crates/bevy_feathers/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "bevy_feathers" +version = "0.17.0-dev" +edition = "2024" +description = "A collection of UI widgets for building editors and utilities in Bevy" +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +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_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", 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_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", 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", features = [ + "bevy_ui_picking_backend", +] } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_winit = { path = "../bevy_winit", version = "0.17.0-dev" } + +# 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_feathers/src/assets/fonts/FiraMono-LICENSE b/crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE new file mode 100644 index 0000000000..5e4608f24a --- /dev/null +++ b/crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE @@ -0,0 +1,93 @@ +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf b/crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf new file mode 100755 index 0000000000..1e95ced4c4 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf new file mode 100644 index 0000000000..e3593fb0f3 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf new file mode 100644 index 0000000000..305b0b8bad Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf new file mode 100644 index 0000000000..27d32ed961 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt b/crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt new file mode 100644 index 0000000000..0d0213ad4c --- /dev/null +++ b/crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt @@ -0,0 +1,93 @@ +Copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000000..6f80647494 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf differ diff --git a/crates/bevy_feathers/src/constants.rs b/crates/bevy_feathers/src/constants.rs new file mode 100644 index 0000000000..359e5a4935 --- /dev/null +++ b/crates/bevy_feathers/src/constants.rs @@ -0,0 +1,35 @@ +//! Various non-themable constants for the Feathers look and feel. + +/// Font asset paths +pub mod fonts { + /// Default regular font path + pub const REGULAR: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Regular.ttf"; + /// Regular italic font path + pub const ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Italic.ttf"; + /// Bold font path + pub const BOLD: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Bold.ttf"; + /// Bold italic font path + pub const BOLD_ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-BoldItalic.ttf"; + /// Monospace font path + pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf"; +} + +/// Size constants +pub mod size { + use bevy_ui::Val; + + /// Common row size for buttons, sliders, spinners, etc. + pub const ROW_HEIGHT: Val = Val::Px(24.0); + + /// Width and height of a checkbox + pub const CHECKBOX_SIZE: Val = Val::Px(18.0); + + /// Width and height of a radio button + pub const RADIO_SIZE: Val = Val::Px(18.0); + + /// Width of a toggle switch + pub const TOGGLE_WIDTH: Val = Val::Px(32.0); + + /// Height of a toggle switch + pub const TOGGLE_HEIGHT: Val = Val::Px(18.0); +} diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs new file mode 100644 index 0000000000..5b6ad7117b --- /dev/null +++ b/crates/bevy_feathers/src/controls/button.rs @@ -0,0 +1,208 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_core_widgets::{Callback, CoreButton}; +use bevy_ecs::{ + bundle::Bundle, + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or}, + schedule::IntoScheduleConfigs, + spawn::{SpawnRelated, SpawnableList}, + system::{Commands, Query}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemeFontColor}, + tokens, +}; + +/// Color variants for buttons. This also functions as a component used by the dynamic styling +/// system to identify which entities are buttons. +#[derive(Component, Default, Clone)] +pub enum ButtonVariant { + /// The standard button appearance + #[default] + Normal, + /// A button with a more prominent color, this is used for "call to action" buttons, + /// default buttons for dialog boxes, and so on. + Primary, +} + +/// Parameters for the button template, passed to [`button`] function. +#[derive(Default)] +pub struct ButtonProps { + /// Color variant for the button. + pub variant: ButtonVariant, + /// Rounded corners options + pub corners: RoundedCorners, + /// Click handler + pub on_click: Callback, +} + +/// Template function to spawn a button. +/// +/// # Arguments +/// * `props` - construction properties for the button. +/// * `overrides` - a bundle of components that are merged in with the normal button components. +/// * `children` - a [`SpawnableList`] of child elements, such as a label or icon for the button. +pub fn button + Send + Sync + 'static, B: Bundle>( + props: ButtonProps, + overrides: B, + children: C, +) -> impl Bundle { + ( + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + flex_grow: 1.0, + ..Default::default() + }, + CoreButton { + on_activate: props.on_click, + }, + props.variant, + Hovered::default(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + props.corners.to_border_radius(4.0), + ThemeBackgroundColor(tokens::BUTTON_BG), + ThemeFontColor(tokens::BUTTON_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font_size: 14.0, + }, + overrides, + Children::spawn(children), + ) +} + +fn update_button_styles( + q_buttons: Query< + ( + Entity, + &ButtonVariant, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + Or<(Changed, Added, Added)>, + >, + mut commands: Commands, +) { + for (button_ent, variant, disabled, pressed, hovered, bg_color, font_color) in q_buttons.iter() + { + set_button_colors( + button_ent, + variant, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } +} + +fn update_button_styles_remove( + q_buttons: Query<( + Entity, + &ButtonVariant, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + )>, + mut removed_disabled: RemovedComponents, + mut removed_pressed: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_pressed.read()) + .for_each(|ent| { + if let Ok((button_ent, variant, disabled, pressed, hovered, bg_color, font_color)) = + q_buttons.get(ent) + { + set_button_colors( + button_ent, + variant, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_button_colors( + button_ent: Entity, + variant: &ButtonVariant, + disabled: bool, + pressed: bool, + hovered: bool, + bg_color: &ThemeBackgroundColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let bg_token = match (variant, disabled, pressed, hovered) { + (ButtonVariant::Normal, true, _, _) => tokens::BUTTON_BG_DISABLED, + (ButtonVariant::Normal, false, true, _) => tokens::BUTTON_BG_PRESSED, + (ButtonVariant::Normal, false, false, true) => tokens::BUTTON_BG_HOVER, + (ButtonVariant::Normal, false, false, false) => tokens::BUTTON_BG, + (ButtonVariant::Primary, true, _, _) => tokens::BUTTON_PRIMARY_BG_DISABLED, + (ButtonVariant::Primary, false, true, _) => tokens::BUTTON_PRIMARY_BG_PRESSED, + (ButtonVariant::Primary, false, false, true) => tokens::BUTTON_PRIMARY_BG_HOVER, + (ButtonVariant::Primary, false, false, false) => tokens::BUTTON_PRIMARY_BG, + }; + + let font_color_token = match (variant, disabled) { + (ButtonVariant::Normal, true) => tokens::BUTTON_TEXT_DISABLED, + (ButtonVariant::Normal, false) => tokens::BUTTON_TEXT, + (ButtonVariant::Primary, true) => tokens::BUTTON_PRIMARY_TEXT_DISABLED, + (ButtonVariant::Primary, false) => tokens::BUTTON_PRIMARY_TEXT, + }; + + // Change background color + if bg_color.0 != bg_token { + commands + .entity(button_ent) + .insert(ThemeBackgroundColor(bg_token)); + } + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(button_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the button styles. +pub struct ButtonPlugin; + +impl Plugin for ButtonPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_button_styles, update_button_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs new file mode 100644 index 0000000000..f81e357c21 --- /dev/null +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -0,0 +1,309 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_core_widgets::{Callback, CoreCheckbox}; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + spawn::{Spawn, SpawnRelated, SpawnableList}, + system::{Commands, In, Query}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_math::Rot2; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_render::view::Visibility; +use bevy_ui::{ + AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, + Node, PositionType, UiRect, UiTransform, Val, +}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; + +/// Parameters for the checkbox template, passed to [`checkbox`] function. +#[derive(Default)] +pub struct CheckboxProps { + /// Change handler + pub on_change: Callback>, +} + +/// Marker for the checkbox frame (contains both checkbox and label) +#[derive(Component, Default, Clone)] +struct CheckboxFrame; + +/// Marker for the checkbox outline +#[derive(Component, Default, Clone)] +struct CheckboxOutline; + +/// Marker for the checkbox check mark +#[derive(Component, Default, Clone)] +struct CheckboxMark; + +/// Template function to spawn a checkbox. +/// +/// # Arguments +/// * `props` - construction properties for the checkbox. +/// * `overrides` - a bundle of components that are merged in with the normal checkbox components. +/// * `label` - the label of the checkbox. +pub fn checkbox + Send + Sync + 'static, B: Bundle>( + props: CheckboxProps, + overrides: B, + label: C, +) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + column_gap: Val::Px(4.0), + ..Default::default() + }, + CoreCheckbox { + on_change: props.on_change, + }, + CheckboxFrame, + Hovered::default(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + ThemeFontColor(tokens::CHECKBOX_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font_size: 14.0, + }, + overrides, + Children::spawn(( + Spawn(( + Node { + width: size::CHECKBOX_SIZE, + height: size::CHECKBOX_SIZE, + border: UiRect::all(Val::Px(2.0)), + ..Default::default() + }, + CheckboxOutline, + BorderRadius::all(Val::Px(4.0)), + ThemeBackgroundColor(tokens::CHECKBOX_BG), + ThemeBorderColor(tokens::CHECKBOX_BORDER), + children![( + // Cheesy checkmark: rotated node with L-shaped border. + Node { + position_type: PositionType::Absolute, + left: Val::Px(4.0), + top: Val::Px(0.0), + width: Val::Px(6.), + height: Val::Px(11.), + border: UiRect { + bottom: Val::Px(2.0), + right: Val::Px(2.0), + ..Default::default() + }, + ..Default::default() + }, + UiTransform::from_rotation(Rot2::FRAC_PI_4), + CheckboxMark, + ThemeBorderColor(tokens::CHECKBOX_MARK), + )], + )), + label, + )), + ) +} + +fn update_checkbox_styles( + q_checkboxes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + q_children: Query<&Children>, + mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With>, + mut q_mark: Query<&ThemeBorderColor, With>, + mut commands: Commands, +) { + for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() { + let Some(outline_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_outline.contains(*en)) + else { + continue; + }; + let Some(mark_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_mark.contains(*en)) + else { + continue; + }; + let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_checkbox_colors( + checkbox_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_bg, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } +} + +fn update_checkbox_styles_remove( + q_checkboxes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + With, + >, + q_children: Query<&Children>, + mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With>, + mut q_mark: Query<&ThemeBorderColor, With>, + mut removed_disabled: RemovedComponents, + mut removed_checked: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_checked.read()) + .for_each(|ent| { + if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) = + q_checkboxes.get(ent) + { + let Some(outline_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_outline.contains(*en)) + else { + return; + }; + let Some(mark_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_mark.contains(*en)) + else { + return; + }; + let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_checkbox_colors( + checkbox_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_bg, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_checkbox_colors( + checkbox_ent: Entity, + outline_ent: Entity, + mark_ent: Entity, + disabled: bool, + checked: bool, + hovered: bool, + outline_bg: &ThemeBackgroundColor, + outline_border: &ThemeBorderColor, + mark_color: &ThemeBorderColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let outline_border_token = match (disabled, hovered) { + (true, _) => tokens::CHECKBOX_BORDER_DISABLED, + (false, true) => tokens::CHECKBOX_BORDER_HOVER, + _ => tokens::CHECKBOX_BORDER, + }; + + let outline_bg_token = match (disabled, checked) { + (true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED, + (true, false) => tokens::CHECKBOX_BG_DISABLED, + (false, true) => tokens::CHECKBOX_BG_CHECKED, + (false, false) => tokens::CHECKBOX_BG, + }; + + let mark_token = match disabled { + true => tokens::CHECKBOX_MARK_DISABLED, + false => tokens::CHECKBOX_MARK, + }; + + let font_color_token = match disabled { + true => tokens::CHECKBOX_TEXT_DISABLED, + false => tokens::CHECKBOX_TEXT, + }; + + // Change outline background + if outline_bg.0 != outline_bg_token { + commands + .entity(outline_ent) + .insert(ThemeBackgroundColor(outline_bg_token)); + } + + // Change outline border + if outline_border.0 != outline_border_token { + commands + .entity(outline_ent) + .insert(ThemeBorderColor(outline_border_token)); + } + + // Change mark color + if mark_color.0 != mark_token { + commands + .entity(mark_ent) + .insert(ThemeBorderColor(mark_token)); + } + + // Change mark visibility + commands.entity(mark_ent).insert(match checked { + true => Visibility::Visible, + false => Visibility::Hidden, + }); + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(checkbox_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the checkbox styles. +pub struct CheckboxPlugin; + +impl Plugin for CheckboxPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs new file mode 100644 index 0000000000..ecad39707b --- /dev/null +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -0,0 +1,29 @@ +//! Meta-module containing all feathers controls (widgets that are interactive). +use bevy_app::Plugin; + +mod button; +mod checkbox; +mod radio; +mod slider; +mod toggle_switch; + +pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; +pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps}; +pub use radio::{radio, RadioPlugin}; +pub use slider::{slider, SliderPlugin, SliderProps}; +pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin, ToggleSwitchProps}; + +/// Plugin which registers all `bevy_feathers` controls. +pub struct ControlsPlugin; + +impl Plugin for ControlsPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_plugins(( + ButtonPlugin, + CheckboxPlugin, + RadioPlugin, + SliderPlugin, + ToggleSwitchPlugin, + )); + } +} diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs new file mode 100644 index 0000000000..a08ffcfa8d --- /dev/null +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -0,0 +1,268 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_core_widgets::CoreRadio; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + spawn::{Spawn, SpawnRelated, SpawnableList}, + system::{Commands, Query}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_render::view::Visibility; +use bevy_ui::{ + AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, + Node, UiRect, Val, +}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; + +/// Marker for the radio outline +#[derive(Component, Default, Clone)] +struct RadioOutline; + +/// Marker for the radio check mark +#[derive(Component, Default, Clone)] +struct RadioMark; + +/// Template function to spawn a radio. +/// +/// # Arguments +/// * `props` - construction properties for the radio. +/// * `overrides` - a bundle of components that are merged in with the normal radio components. +/// * `label` - the label of the radio. +pub fn radio + Send + Sync + 'static, B: Bundle>( + overrides: B, + label: C, +) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + column_gap: Val::Px(4.0), + ..Default::default() + }, + CoreRadio, + Hovered::default(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + ThemeFontColor(tokens::RADIO_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font_size: 14.0, + }, + overrides, + Children::spawn(( + Spawn(( + Node { + display: Display::Flex, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: size::RADIO_SIZE, + height: size::RADIO_SIZE, + border: UiRect::all(Val::Px(2.0)), + ..Default::default() + }, + RadioOutline, + BorderRadius::MAX, + ThemeBorderColor(tokens::RADIO_BORDER), + children![( + // Cheesy checkmark: rotated node with L-shaped border. + Node { + width: Val::Px(8.), + height: Val::Px(8.), + ..Default::default() + }, + BorderRadius::MAX, + RadioMark, + ThemeBackgroundColor(tokens::RADIO_MARK), + )], + )), + label, + )), + ) +} + +fn update_radio_styles( + q_radioes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + q_children: Query<&Children>, + mut q_outline: Query<&ThemeBorderColor, With>, + mut q_mark: Query<&ThemeBackgroundColor, With>, + mut commands: Commands, +) { + for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() { + let Some(outline_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_outline.contains(*en)) + else { + continue; + }; + let Some(mark_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_mark.contains(*en)) + else { + continue; + }; + let outline_border = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_radio_colors( + radio_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } +} + +fn update_radio_styles_remove( + q_radioes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + With, + >, + q_children: Query<&Children>, + mut q_outline: Query<&ThemeBorderColor, With>, + mut q_mark: Query<&ThemeBackgroundColor, With>, + mut removed_disabled: RemovedComponents, + mut removed_checked: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_checked.read()) + .for_each(|ent| { + if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) { + let Some(outline_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_outline.contains(*en)) + else { + return; + }; + let Some(mark_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_mark.contains(*en)) + else { + return; + }; + let outline_border = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_radio_colors( + radio_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_radio_colors( + radio_ent: Entity, + outline_ent: Entity, + mark_ent: Entity, + disabled: bool, + checked: bool, + hovered: bool, + outline_border: &ThemeBorderColor, + mark_color: &ThemeBackgroundColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let outline_border_token = match (disabled, hovered) { + (true, _) => tokens::RADIO_BORDER_DISABLED, + (false, true) => tokens::RADIO_BORDER_HOVER, + _ => tokens::RADIO_BORDER, + }; + + let mark_token = match disabled { + true => tokens::RADIO_MARK_DISABLED, + false => tokens::RADIO_MARK, + }; + + let font_color_token = match disabled { + true => tokens::RADIO_TEXT_DISABLED, + false => tokens::RADIO_TEXT, + }; + + // Change outline border + if outline_border.0 != outline_border_token { + commands + .entity(outline_ent) + .insert(ThemeBorderColor(outline_border_token)); + } + + // Change mark color + if mark_color.0 != mark_token { + commands + .entity(mark_ent) + .insert(ThemeBorderColor(mark_token)); + } + + // Change mark visibility + commands.entity(mark_ent).insert(match checked { + true => Visibility::Visible, + false => Visibility::Hidden, + }); + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(radio_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the radio styles. +pub struct RadioPlugin; + +impl Plugin for RadioPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs new file mode 100644 index 0000000000..fa1978e06c --- /dev/null +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -0,0 +1,208 @@ +use core::f32::consts::PI; + +use bevy_app::{Plugin, PreUpdate}; +use bevy_color::Color; +use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick}; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::Children, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or, Spawned, With}, + schedule::IntoScheduleConfigs, + spawn::SpawnRelated, + system::{In, Query, Res}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_picking::PickingSystems; +use bevy_ui::{ + widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient, + InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect, + Val, +}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + rounded_corners::RoundedCorners, + theme::{ThemeFontColor, ThemedText, UiTheme}, + tokens, +}; + +/// Slider template properties, passed to [`slider`] function. +pub struct SliderProps { + /// Slider current value + pub value: f32, + /// Slider minimum value + pub min: f32, + /// Slider maximum value + pub max: f32, + /// On-change handler + pub on_change: Callback>, +} + +impl Default for SliderProps { + fn default() -> Self { + Self { + value: 0.0, + min: 0.0, + max: 1.0, + on_change: Callback::Ignore, + } + } +} + +#[derive(Component, Default, Clone)] +#[require(CoreSlider)] +struct SliderStyle; + +/// Marker for the text +#[derive(Component, Default, Clone)] +struct SliderValueText; + +/// Spawn a new slider widget. +/// +/// # Arguments +/// +/// * `props` - construction properties for the slider. +/// * `overrides` - a bundle of components that are merged in with the normal slider components. +pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { + ( + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + flex_grow: 1.0, + ..Default::default() + }, + CoreSlider { + on_change: props.on_change, + track_click: TrackClick::Drag, + }, + SliderStyle, + SliderValue(props.value), + SliderRange::new(props.min, props.max), + CursorIcon::System(bevy_window::SystemCursorIcon::EwResize), + TabIndex(0), + RoundedCorners::All.to_border_radius(6.0), + // Use a gradient to draw the moving bar + BackgroundGradient(vec![Gradient::Linear(LinearGradient { + angle: PI * 0.5, + stops: vec![ + ColorStop::new(Color::NONE, Val::Percent(0.)), + ColorStop::new(Color::NONE, Val::Percent(50.)), + ColorStop::new(Color::NONE, Val::Percent(50.)), + ColorStop::new(Color::NONE, Val::Percent(100.)), + ], + color_space: InterpolationColorSpace::Srgb, + })]), + overrides, + children![( + // Text container + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..Default::default() + }, + ThemeFontColor(tokens::SLIDER_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::MONO.to_owned()), + font_size: 12.0, + }, + children![(Text::new("10.0"), ThemedText, SliderValueText,)], + )], + ) +} + +fn update_slider_colors( + mut q_sliders: Query< + (Has, &mut BackgroundGradient), + (With, Or<(Spawned, Added)>), + >, + theme: Res, +) { + for (disabled, mut gradient) in q_sliders.iter_mut() { + set_slider_colors(&theme, disabled, gradient.as_mut()); + } +} + +fn update_slider_colors_remove( + mut q_sliders: Query<(Has, &mut BackgroundGradient)>, + mut removed_disabled: RemovedComponents, + theme: Res, +) { + removed_disabled.read().for_each(|ent| { + if let Ok((disabled, mut gradient)) = q_sliders.get_mut(ent) { + set_slider_colors(&theme, disabled, gradient.as_mut()); + } + }); +} + +fn set_slider_colors(theme: &Res<'_, UiTheme>, disabled: bool, gradient: &mut BackgroundGradient) { + let bar_color = theme.color(match disabled { + true => tokens::SLIDER_BAR_DISABLED, + false => tokens::SLIDER_BAR, + }); + let bg_color = theme.color(tokens::SLIDER_BG); + if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] { + linear_gradient.stops[0].color = bar_color; + linear_gradient.stops[1].color = bar_color; + linear_gradient.stops[2].color = bg_color; + linear_gradient.stops[3].color = bg_color; + } +} + +fn update_slider_pos( + mut q_sliders: Query< + (Entity, &SliderValue, &SliderRange, &mut BackgroundGradient), + ( + With, + Or<( + Changed, + Changed, + Changed, + )>, + ), + >, + q_children: Query<&Children>, + mut q_slider_text: Query<&mut Text, With>, +) { + for (slider_ent, value, range, mut gradient) in q_sliders.iter_mut() { + if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] { + let percent_value = range.thumb_position(value.0) * 100.0; + linear_gradient.stops[1].point = Val::Percent(percent_value); + linear_gradient.stops[2].point = Val::Percent(percent_value); + } + + // Find slider text child entity and update its text with the formatted value + q_children.iter_descendants(slider_ent).for_each(|child| { + if let Ok(mut text) = q_slider_text.get_mut(child) { + text.0 = format!("{}", value.0); + } + }); + } +} + +/// Plugin which registers the systems for updating the slider styles. +pub struct SliderPlugin; + +impl Plugin for SliderPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + ( + update_slider_colors, + update_slider_colors_remove, + update_slider_pos, + ) + .in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs new file mode 100644 index 0000000000..bc473d8d81 --- /dev/null +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -0,0 +1,249 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{Plugin, PreUpdate}; +use bevy_core_widgets::{Callback, CoreCheckbox}; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::Children, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + spawn::SpawnRelated, + system::{Commands, In, Query}, + world::Mut, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::size, + theme::{ThemeBackgroundColor, ThemeBorderColor}, + tokens, +}; + +/// Parameters for the toggle switch template, passed to [`toggle_switch`] function. +#[derive(Default)] +pub struct ToggleSwitchProps { + /// Change handler + pub on_change: Callback>, +} + +/// Marker for the toggle switch outline +#[derive(Component, Default, Clone)] +struct ToggleSwitchOutline; + +/// Marker for the toggle switch slide +#[derive(Component, Default, Clone)] +struct ToggleSwitchSlide; + +/// Template function to spawn a toggle switch. +/// +/// # Arguments +/// * `props` - construction properties for the toggle switch. +/// * `overrides` - a bundle of components that are merged in with the normal toggle switch components. +pub fn toggle_switch(props: ToggleSwitchProps, overrides: B) -> impl Bundle { + ( + Node { + width: size::TOGGLE_WIDTH, + height: size::TOGGLE_HEIGHT, + border: UiRect::all(Val::Px(2.0)), + ..Default::default() + }, + CoreCheckbox { + on_change: props.on_change, + }, + ToggleSwitchOutline, + BorderRadius::all(Val::Px(5.0)), + ThemeBackgroundColor(tokens::SWITCH_BG), + ThemeBorderColor(tokens::SWITCH_BORDER), + AccessibilityNode(accesskit::Node::new(Role::Switch)), + Hovered::default(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + overrides, + children![( + Node { + position_type: PositionType::Absolute, + left: Val::Percent(0.), + top: Val::Px(0.), + bottom: Val::Px(0.), + width: Val::Percent(50.), + ..Default::default() + }, + BorderRadius::all(Val::Px(3.0)), + ToggleSwitchSlide, + ThemeBackgroundColor(tokens::SWITCH_SLIDE), + )], + ) +} + +fn update_switch_styles( + q_switches: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeBorderColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + q_children: Query<&Children>, + mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With>, + mut commands: Commands, +) { + for (switch_ent, disabled, checked, hovered, outline_bg, outline_border) in q_switches.iter() { + let Some(slide_ent) = q_children + .iter_descendants(switch_ent) + .find(|en| q_slide.contains(*en)) + else { + continue; + }; + // Safety: since we just checked the query, should always work. + let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap(); + set_switch_colors( + switch_ent, + slide_ent, + disabled, + checked, + hovered.0, + outline_bg, + outline_border, + slide_style, + slide_color, + &mut commands, + ); + } +} + +fn update_switch_styles_remove( + q_switches: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeBorderColor, + ), + With, + >, + q_children: Query<&Children>, + mut q_slide: Query<(&mut Node, &ThemeBackgroundColor), With>, + mut removed_disabled: RemovedComponents, + mut removed_checked: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_checked.read()) + .for_each(|ent| { + if let Ok((switch_ent, disabled, checked, hovered, outline_bg, outline_border)) = + q_switches.get(ent) + { + let Some(slide_ent) = q_children + .iter_descendants(switch_ent) + .find(|en| q_slide.contains(*en)) + else { + return; + }; + // Safety: since we just checked the query, should always work. + let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap(); + set_switch_colors( + switch_ent, + slide_ent, + disabled, + checked, + hovered.0, + outline_bg, + outline_border, + slide_style, + slide_color, + &mut commands, + ); + } + }); +} + +fn set_switch_colors( + switch_ent: Entity, + slide_ent: Entity, + disabled: bool, + checked: bool, + hovered: bool, + outline_bg: &ThemeBackgroundColor, + outline_border: &ThemeBorderColor, + slide_style: &mut Mut, + slide_color: &ThemeBackgroundColor, + commands: &mut Commands, +) { + let outline_border_token = match (disabled, hovered) { + (true, _) => tokens::SWITCH_BORDER_DISABLED, + (false, true) => tokens::SWITCH_BORDER_HOVER, + _ => tokens::SWITCH_BORDER, + }; + + let outline_bg_token = match (disabled, checked) { + (true, true) => tokens::SWITCH_BG_CHECKED_DISABLED, + (true, false) => tokens::SWITCH_BG_DISABLED, + (false, true) => tokens::SWITCH_BG_CHECKED, + (false, false) => tokens::SWITCH_BG, + }; + + let slide_token = match disabled { + true => tokens::SWITCH_SLIDE_DISABLED, + false => tokens::SWITCH_SLIDE, + }; + + let slide_pos = match checked { + true => Val::Percent(50.), + false => Val::Percent(0.), + }; + + // Change outline background + if outline_bg.0 != outline_bg_token { + commands + .entity(switch_ent) + .insert(ThemeBackgroundColor(outline_bg_token)); + } + + // Change outline border + if outline_border.0 != outline_border_token { + commands + .entity(switch_ent) + .insert(ThemeBorderColor(outline_border_token)); + } + + // Change slide color + if slide_color.0 != slide_token { + commands + .entity(slide_ent) + .insert(ThemeBackgroundColor(slide_token)); + } + + // Change slide position + if slide_pos != slide_style.left { + slide_style.left = slide_pos; + } +} + +/// Plugin which registers the systems for updating the toggle switch styles. +pub struct ToggleSwitchPlugin; + +impl Plugin for ToggleSwitchPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_switch_styles, update_switch_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs new file mode 100644 index 0000000000..a9811edfb2 --- /dev/null +++ b/crates/bevy_feathers/src/cursor.rs @@ -0,0 +1,70 @@ +//! Provides a way to automatically set the mouse cursor based on hovered entity. +use bevy_app::{App, Plugin, PreUpdate}; +use bevy_ecs::{ + entity::Entity, + hierarchy::ChildOf, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res}, +}; +use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; +use bevy_window::Window; +use bevy_winit::cursor::CursorIcon; + +/// A component that specifies the cursor icon to be used when the mouse is not hovering over +/// any other entity. This is used to set the default cursor icon for the window. +#[derive(Resource, Debug, Clone, Default)] +pub struct DefaultCursorIcon(pub CursorIcon); + +/// System which updates the window cursor icon whenever the mouse hovers over an entity with +/// a [`CursorIcon`] component. If no entity is hovered, the cursor icon is set to +/// the cursor in the [`DefaultCursorIcon`] resource. +pub(crate) fn update_cursor( + mut commands: Commands, + hover_map: Option>, + parent_query: Query<&ChildOf>, + cursor_query: Query<&CursorIcon>, + mut q_windows: Query<(Entity, &mut Window, Option<&CursorIcon>)>, + r_default_cursor: Res, +) { + let cursor = hover_map.and_then(|hover_map| match hover_map.get(&PointerId::Mouse) { + Some(hover_set) => hover_set.keys().find_map(|entity| { + cursor_query.get(*entity).ok().or_else(|| { + parent_query + .iter_ancestors(*entity) + .find_map(|e| cursor_query.get(e).ok()) + }) + }), + None => None, + }); + + let mut windows_to_change: Vec = Vec::new(); + for (entity, _window, prev_cursor) in q_windows.iter_mut() { + match (cursor, prev_cursor) { + (Some(cursor), Some(prev_cursor)) if cursor == prev_cursor => continue, + (None, None) => continue, + _ => { + windows_to_change.push(entity); + } + } + } + windows_to_change.iter().for_each(|entity| { + if let Some(cursor) = cursor { + commands.entity(*entity).insert(cursor.clone()); + } else { + commands.entity(*entity).insert(r_default_cursor.0.clone()); + } + }); +} + +/// Plugin that supports automatically changing the cursor based on the hovered entity. +pub struct CursorIconPlugin; + +impl Plugin for CursorIconPlugin { + fn build(&self, app: &mut App) { + if app.world().get_resource::().is_none() { + app.init_resource::(); + } + app.add_systems(PreUpdate, update_cursor.in_set(PickingSystems::Last)); + } +} diff --git a/crates/bevy_feathers/src/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs new file mode 100644 index 0000000000..c3ff4e4204 --- /dev/null +++ b/crates/bevy_feathers/src/dark_theme.rs @@ -0,0 +1,127 @@ +//! The standard `bevy_feathers` dark theme. +use crate::{palette, tokens}; +use bevy_color::{Alpha, Luminance}; +use bevy_platform::collections::HashMap; + +use crate::theme::ThemeProps; + +/// Create a [`ThemeProps`] object and populate it with the colors for the default dark theme. +pub fn create_dark_theme() -> ThemeProps { + ThemeProps { + color: HashMap::from([ + (tokens::WINDOW_BG.into(), palette::GRAY_0), + // Button + (tokens::BUTTON_BG.into(), palette::GRAY_3), + ( + tokens::BUTTON_BG_HOVER.into(), + palette::GRAY_3.lighter(0.05), + ), + ( + tokens::BUTTON_BG_PRESSED.into(), + palette::GRAY_3.lighter(0.1), + ), + (tokens::BUTTON_BG_DISABLED.into(), palette::GRAY_2), + (tokens::BUTTON_PRIMARY_BG.into(), palette::ACCENT), + ( + tokens::BUTTON_PRIMARY_BG_HOVER.into(), + palette::ACCENT.lighter(0.05), + ), + ( + tokens::BUTTON_PRIMARY_BG_PRESSED.into(), + palette::ACCENT.lighter(0.1), + ), + (tokens::BUTTON_PRIMARY_BG_DISABLED.into(), palette::GRAY_2), + (tokens::BUTTON_TEXT.into(), palette::WHITE), + ( + tokens::BUTTON_TEXT_DISABLED.into(), + palette::WHITE.with_alpha(0.5), + ), + (tokens::BUTTON_PRIMARY_TEXT.into(), palette::WHITE), + ( + tokens::BUTTON_PRIMARY_TEXT_DISABLED.into(), + palette::WHITE.with_alpha(0.5), + ), + // Slider + (tokens::SLIDER_BG.into(), palette::GRAY_1), + (tokens::SLIDER_BAR.into(), palette::ACCENT), + (tokens::SLIDER_BAR_DISABLED.into(), palette::GRAY_2), + (tokens::SLIDER_TEXT.into(), palette::WHITE), + ( + tokens::SLIDER_TEXT_DISABLED.into(), + palette::WHITE.with_alpha(0.5), + ), + // Checkbox + (tokens::CHECKBOX_BG.into(), palette::GRAY_3), + (tokens::CHECKBOX_BG_CHECKED.into(), palette::ACCENT), + ( + tokens::CHECKBOX_BG_DISABLED.into(), + palette::GRAY_1.with_alpha(0.5), + ), + ( + tokens::CHECKBOX_BG_CHECKED_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::CHECKBOX_BORDER.into(), palette::GRAY_3), + ( + tokens::CHECKBOX_BORDER_HOVER.into(), + palette::GRAY_3.lighter(0.1), + ), + ( + tokens::CHECKBOX_BORDER_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::CHECKBOX_MARK.into(), palette::WHITE), + (tokens::CHECKBOX_MARK_DISABLED.into(), palette::LIGHT_GRAY_2), + (tokens::CHECKBOX_TEXT.into(), palette::LIGHT_GRAY_1), + ( + tokens::CHECKBOX_TEXT_DISABLED.into(), + palette::LIGHT_GRAY_1.with_alpha(0.5), + ), + // Radio + (tokens::RADIO_BORDER.into(), palette::GRAY_3), + ( + tokens::RADIO_BORDER_HOVER.into(), + palette::GRAY_3.lighter(0.1), + ), + ( + tokens::RADIO_BORDER_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::RADIO_MARK.into(), palette::ACCENT), + ( + tokens::RADIO_MARK_DISABLED.into(), + palette::ACCENT.with_alpha(0.5), + ), + (tokens::RADIO_TEXT.into(), palette::LIGHT_GRAY_1), + ( + tokens::RADIO_TEXT_DISABLED.into(), + palette::LIGHT_GRAY_1.with_alpha(0.5), + ), + // Toggle Switch + (tokens::SWITCH_BG.into(), palette::GRAY_3), + (tokens::SWITCH_BG_CHECKED.into(), palette::ACCENT), + ( + tokens::SWITCH_BG_DISABLED.into(), + palette::GRAY_1.with_alpha(0.5), + ), + ( + tokens::SWITCH_BG_CHECKED_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::SWITCH_BORDER.into(), palette::GRAY_3), + ( + tokens::SWITCH_BORDER_HOVER.into(), + palette::GRAY_3.lighter(0.1), + ), + ( + tokens::SWITCH_BORDER_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::SWITCH_SLIDE.into(), palette::LIGHT_GRAY_2), + ( + tokens::SWITCH_SLIDE_DISABLED.into(), + palette::LIGHT_GRAY_2.with_alpha(0.3), + ), + ]), + } +} diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs new file mode 100644 index 0000000000..6de064dd39 --- /dev/null +++ b/crates/bevy_feathers/src/font_styles.rs @@ -0,0 +1,62 @@ +//! A framework for inheritable font styles. +use bevy_app::Propagate; +use bevy_asset::{AssetServer, Handle}; +use bevy_ecs::{ + component::Component, + lifecycle::Insert, + observer::On, + system::{Commands, Query, Res}, +}; +use bevy_text::{Font, TextFont}; + +use crate::handle_or_path::HandleOrPath; + +/// A component which, when inserted on an entity, will load the given font and propagate it +/// downward to any child text entity that has the [`ThemedText`](crate::theme::ThemedText) marker. +#[derive(Component, Default, Clone, Debug)] +pub struct InheritableFont { + /// The font handle or path. + pub font: HandleOrPath, + /// The desired font size. + pub font_size: f32, +} + +impl InheritableFont { + /// Create a new `InheritableFont` from a handle. + pub fn from_handle(handle: Handle) -> Self { + Self { + font: HandleOrPath::Handle(handle), + font_size: 16.0, + } + } + + /// Create a new `InheritableFont` from a path. + pub fn from_path(path: &str) -> Self { + Self { + font: HandleOrPath::Path(path.to_string()), + font_size: 16.0, + } + } +} + +/// An observer which looks for changes to the `InheritableFont` component on an entity, and +/// propagates downward the font to all participating text entities. +pub(crate) fn on_changed_font( + ev: On, + font_style: Query<&InheritableFont>, + assets: Res, + mut commands: Commands, +) { + if let Ok(style) = font_style.get(ev.target()) { + if let Some(font) = match style.font { + HandleOrPath::Handle(ref h) => Some(h.clone()), + HandleOrPath::Path(ref p) => Some(assets.load::(p)), + } { + commands.entity(ev.target()).insert(Propagate(TextFont { + font, + font_size: style.font_size, + ..Default::default() + })); + } + } +} diff --git a/crates/bevy_feathers/src/handle_or_path.rs b/crates/bevy_feathers/src/handle_or_path.rs new file mode 100644 index 0000000000..178d2b13e8 --- /dev/null +++ b/crates/bevy_feathers/src/handle_or_path.rs @@ -0,0 +1,61 @@ +//! Provides a way to specify assets either by handle or by path. +use bevy_asset::{Asset, Handle}; + +/// Enum that represents a reference to an asset as either a [`Handle`] or a [`String`] path. +/// +/// This is useful for when you want to specify an asset, but don't always have convenient +/// access to an asset server reference. +#[derive(Clone, Debug)] +pub enum HandleOrPath { + /// Specify the asset reference as a handle. + Handle(Handle), + /// Specify the asset reference as a [`String`]. + Path(String), +} + +impl Default for HandleOrPath { + fn default() -> Self { + Self::Path("".to_string()) + } +} + +// Necessary because we don't want to require T: PartialEq +impl PartialEq for HandleOrPath { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (HandleOrPath::Handle(h1), HandleOrPath::Handle(h2)) => h1 == h2, + (HandleOrPath::Path(p1), HandleOrPath::Path(p2)) => p1 == p2, + _ => false, + } + } +} + +impl From> for HandleOrPath { + fn from(h: Handle) -> Self { + HandleOrPath::Handle(h) + } +} + +impl From<&str> for HandleOrPath { + fn from(p: &str) -> Self { + HandleOrPath::Path(p.to_string()) + } +} + +impl From for HandleOrPath { + fn from(p: String) -> Self { + HandleOrPath::Path(p.clone()) + } +} + +impl From<&String> for HandleOrPath { + fn from(p: &String) -> Self { + HandleOrPath::Path(p.to_string()) + } +} + +impl From<&HandleOrPath> for HandleOrPath { + fn from(p: &HandleOrPath) -> Self { + p.to_owned() + } +} diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs new file mode 100644 index 0000000000..ab02304a85 --- /dev/null +++ b/crates/bevy_feathers/src/lib.rs @@ -0,0 +1,74 @@ +//! `bevy_feathers` is a collection of styled and themed widgets for building editors and +//! inspectors. +//! +//! The aesthetic choices made here are designed with a future Bevy Editor in mind, +//! but this crate is deliberately exposed to the public to allow the broader ecosystem to easily create +//! tooling for themselves and others that fits cohesively together. +//! +//! While it may be tempting to use this crate for your game's UI, it's deliberately not intended for that. +//! We've opted for a clean, functional style, and prioritized consistency over customization. +//! That said, if you like what you see, it can be a helpful learning tool. +//! Consider copying this code into your own project, +//! and refining the styles and abstractions provided to meet your needs. +//! +//! ## Warning: Experimental! +//! All that said, this crate is still experimental and unfinished! +//! It will change in breaking ways, and there will be both bugs and limitations. +//! +//! Please report issues, submit fixes and propose changes. +//! Thanks for stress-testing; let's build something better together. + +use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate}; +use bevy_asset::embedded_asset; +use bevy_ecs::query::With; +use bevy_text::{TextColor, TextFont}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + controls::ControlsPlugin, + cursor::{CursorIconPlugin, DefaultCursorIcon}, + theme::{ThemedText, UiTheme}, +}; + +pub mod constants; +pub mod controls; +pub mod cursor; +pub mod dark_theme; +pub mod font_styles; +pub mod handle_or_path; +pub mod palette; +pub mod rounded_corners; +pub mod theme; +pub mod tokens; + +/// Plugin which installs observers and systems for feathers themes, cursors, and all controls. +pub struct FeathersPlugin; + +impl Plugin for FeathersPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.init_resource::(); + + embedded_asset!(app, "assets/fonts/FiraSans-Bold.ttf"); + embedded_asset!(app, "assets/fonts/FiraSans-BoldItalic.ttf"); + embedded_asset!(app, "assets/fonts/FiraSans-Regular.ttf"); + embedded_asset!(app, "assets/fonts/FiraSans-Italic.ttf"); + embedded_asset!(app, "assets/fonts/FiraMono-Medium.ttf"); + + app.add_plugins(( + ControlsPlugin, + CursorIconPlugin, + HierarchyPropagatePlugin::>::default(), + HierarchyPropagatePlugin::>::default(), + )); + + app.insert_resource(DefaultCursorIcon(CursorIcon::System( + bevy_window::SystemCursorIcon::Default, + ))); + + app.add_systems(PostUpdate, theme::update_theme) + .add_observer(theme::on_changed_background) + .add_observer(theme::on_changed_border) + .add_observer(theme::on_changed_font_color) + .add_observer(font_styles::on_changed_font); + } +} diff --git a/crates/bevy_feathers/src/palette.rs b/crates/bevy_feathers/src/palette.rs new file mode 100644 index 0000000000..bc2f6d51c6 --- /dev/null +++ b/crates/bevy_feathers/src/palette.rs @@ -0,0 +1,29 @@ +//! The Feathers standard color palette. +use bevy_color::Color; + +///

+pub const BLACK: Color = Color::oklcha(0.0, 0.0, 0.0, 1.0); +///
- window background +pub const GRAY_0: Color = Color::oklcha(0.2414, 0.0095, 285.67, 1.0); +///
- pane background +pub const GRAY_1: Color = Color::oklcha(0.2866, 0.0072, 285.93, 1.0); +///
- item background +pub const GRAY_2: Color = Color::oklcha(0.3373, 0.0071, 274.77, 1.0); +///
- item background (active) +pub const GRAY_3: Color = Color::oklcha(0.3992, 0.0101, 278.38, 1.0); +///
- border +pub const WARM_GRAY_1: Color = Color::oklcha(0.3757, 0.0017, 286.32, 1.0); +///
- bright label text +pub const LIGHT_GRAY_1: Color = Color::oklcha(0.7607, 0.0014, 286.37, 1.0); +///
- dim label text +pub const LIGHT_GRAY_2: Color = Color::oklcha(0.6106, 0.003, 286.31, 1.0); +///
- button label text +pub const WHITE: Color = Color::oklcha(1.0, 0.000000059604645, 90.0, 1.0); +///
- call-to-action and selection color +pub const ACCENT: Color = Color::oklcha(0.542, 0.1594, 255.4, 1.0); +///
- for X-axis inputs and drag handles +pub const X_AXIS: Color = Color::oklcha(0.5232, 0.1404, 13.84, 1.0); +///
- for Y-axis inputs and drag handles +pub const Y_AXIS: Color = Color::oklcha(0.5866, 0.1543, 129.84, 1.0); +///
- for Z-axis inputs and drag handles +pub const Z_AXIS: Color = Color::oklcha(0.4847, 0.1249, 253.08, 1.0); diff --git a/crates/bevy_feathers/src/rounded_corners.rs b/crates/bevy_feathers/src/rounded_corners.rs new file mode 100644 index 0000000000..4d2be9e0a8 --- /dev/null +++ b/crates/bevy_feathers/src/rounded_corners.rs @@ -0,0 +1,96 @@ +//! Mechanism for specifying which corners of a widget are rounded, used for segmented buttons +//! and control groups. +use bevy_ui::{BorderRadius, Val}; + +/// Allows specifying which corners are rounded and which are sharp. All rounded corners +/// have the same radius. Not all combinations are supported, only the ones that make +/// sense for a segmented buttons. +/// +/// A typical use case would be a segmented button consisting of 3 individual buttons in a +/// row. In that case, you would have the leftmost button have rounded corners on the left, +/// the right-most button have rounded corners on the right, and the center button have +/// only sharp corners. +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub enum RoundedCorners { + /// No corners are rounded. + None, + #[default] + /// All corners are rounded. + All, + /// Top-left corner is rounded. + TopLeft, + /// Top-right corner is rounded. + TopRight, + /// Bottom-right corner is rounded. + BottomRight, + /// Bottom-left corner is rounded. + BottomLeft, + /// Top corners are rounded. + Top, + /// Right corners are rounded. + Right, + /// Bottom corners are rounded. + Bottom, + /// Left corners are rounded. + Left, +} + +impl RoundedCorners { + /// Convert the `RoundedCorners` to a `BorderRadius` for use in a `Node`. + pub fn to_border_radius(&self, radius: f32) -> BorderRadius { + let radius = Val::Px(radius); + let zero = Val::ZERO; + match self { + RoundedCorners::None => BorderRadius::all(zero), + RoundedCorners::All => BorderRadius::all(radius), + RoundedCorners::TopLeft => BorderRadius { + top_left: radius, + top_right: zero, + bottom_right: zero, + bottom_left: zero, + }, + RoundedCorners::TopRight => BorderRadius { + top_left: zero, + top_right: radius, + bottom_right: zero, + bottom_left: zero, + }, + RoundedCorners::BottomRight => BorderRadius { + top_left: zero, + top_right: zero, + bottom_right: radius, + bottom_left: zero, + }, + RoundedCorners::BottomLeft => BorderRadius { + top_left: zero, + top_right: zero, + bottom_right: zero, + bottom_left: radius, + }, + RoundedCorners::Top => BorderRadius { + top_left: radius, + top_right: radius, + bottom_right: zero, + bottom_left: zero, + }, + RoundedCorners::Right => BorderRadius { + top_left: zero, + top_right: radius, + bottom_right: radius, + bottom_left: zero, + }, + RoundedCorners::Bottom => BorderRadius { + top_left: zero, + top_right: zero, + bottom_right: radius, + bottom_left: radius, + }, + RoundedCorners::Left => BorderRadius { + top_left: radius, + top_right: zero, + bottom_right: zero, + bottom_left: radius, + }, + } + } +} diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs new file mode 100644 index 0000000000..9969b54846 --- /dev/null +++ b/crates/bevy_feathers/src/theme.rs @@ -0,0 +1,131 @@ +//! A framework for theming. +use bevy_app::Propagate; +use bevy_color::{palettes, Color}; +use bevy_ecs::{ + change_detection::DetectChanges, + component::Component, + lifecycle::Insert, + observer::On, + query::Changed, + resource::Resource, + system::{Commands, Query, Res}, +}; +use bevy_log::warn_once; +use bevy_platform::collections::HashMap; +use bevy_text::TextColor; +use bevy_ui::{BackgroundColor, BorderColor}; + +/// A collection of properties that make up a theme. +#[derive(Default, Clone)] +pub struct ThemeProps { + /// Map of design tokens to colors. + pub color: HashMap, + // Other style property types to be added later. +} + +/// The currently selected user interface theme. Overwriting this resource changes the theme. +#[derive(Resource, Default)] +pub struct UiTheme(pub ThemeProps); + +impl UiTheme { + /// Lookup a color by design token. If the theme does not have an entry for that token, + /// logs a warning and returns an error color. + pub fn color<'a>(&self, token: &'a str) -> Color { + let color = self.0.color.get(token); + match color { + Some(c) => *c, + None => { + warn_once!("Theme color {} not found.", token); + // Return a bright obnoxious color to make the error obvious. + palettes::basic::FUCHSIA.into() + } + } + } + + /// Associate a design token with a given color. + pub fn set_color(&mut self, token: impl Into, color: Color) { + self.0.color.insert(token.into(), color); + } +} + +/// Component which causes the background color of an entity to be set based on a theme color. +#[derive(Component, Clone, Copy)] +#[require(BackgroundColor)] +#[component(immutable)] +pub struct ThemeBackgroundColor(pub &'static str); + +/// Component which causes the border color of an entity to be set based on a theme color. +/// Only supports setting all borders to the same color. +#[derive(Component, Clone, Copy)] +#[require(BorderColor)] +#[component(immutable)] +pub struct ThemeBorderColor(pub &'static str); + +/// Component which causes the inherited text color of an entity to be set based on a theme color. +#[derive(Component, Clone, Copy)] +#[component(immutable)] +pub struct ThemeFontColor(pub &'static str); + +/// A marker component that is used to indicate that the text entity wants to opt-in to using +/// inherited text styles. +#[derive(Component)] +pub struct ThemedText; + +pub(crate) fn update_theme( + mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>, + mut q_border: Query<(&mut BorderColor, &ThemeBorderColor)>, + theme: Res, +) { + if theme.is_changed() { + // Update all background colors + for (mut bg, theme_bg) in q_background.iter_mut() { + bg.0 = theme.color(theme_bg.0); + } + + // Update all border colors + for (mut border, theme_border) in q_border.iter_mut() { + border.set_all(theme.color(theme_border.0)); + } + } +} + +pub(crate) fn on_changed_background( + ev: On, + mut q_background: Query< + (&mut BackgroundColor, &ThemeBackgroundColor), + Changed, + >, + theme: Res, +) { + // Update background colors where the design token has changed. + if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.target()) { + bg.0 = theme.color(theme_bg.0); + } +} + +pub(crate) fn on_changed_border( + ev: On, + mut q_border: Query<(&mut BorderColor, &ThemeBorderColor), Changed>, + theme: Res, +) { + // Update background colors where the design token has changed. + if let Ok((mut border, theme_border)) = q_border.get_mut(ev.target()) { + border.set_all(theme.color(theme_border.0)); + } +} + +/// An observer which looks for changes to the [`ThemeFontColor`] component on an entity, and +/// propagates downward the text color to all participating text entities. +pub(crate) fn on_changed_font_color( + ev: On, + font_color: Query<&ThemeFontColor>, + theme: Res, + mut commands: Commands, +) { + if let Ok(token) = font_color.get(ev.target()) { + let color = theme.color(token.0); + commands + .entity(ev.target()) + .insert(Propagate(TextColor(color))); + } +} diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs new file mode 100644 index 0000000000..453dc94c5e --- /dev/null +++ b/crates/bevy_feathers/src/tokens.rs @@ -0,0 +1,122 @@ +//! Design tokens used by Feathers themes. +//! +//! The term "design token" is commonly used in UX design to mean the smallest unit of a theme, +//! similar in concept to a CSS variable. Each token represents an assignment of a color or +//! value to a specific visual aspect of a widget, such as background or border. + +/// Window background +pub const WINDOW_BG: &str = "feathers.window.bg"; + +/// Focus ring +pub const FOCUS_RING: &str = "feathers.focus"; + +/// Regular text +pub const TEXT_MAIN: &str = "feathers.text.main"; +/// Dim text +pub const TEXT_DIM: &str = "feathers.text.dim"; + +// Normal buttons + +/// Regular button background +pub const BUTTON_BG: &str = "feathers.button.bg"; +/// Regular button background (hovered) +pub const BUTTON_BG_HOVER: &str = "feathers.button.bg.hover"; +/// Regular button background (disabled) +pub const BUTTON_BG_DISABLED: &str = "feathers.button.bg.disabled"; +/// Regular button background (pressed) +pub const BUTTON_BG_PRESSED: &str = "feathers.button.bg.pressed"; +/// Regular button text +pub const BUTTON_TEXT: &str = "feathers.button.txt"; +/// Regular button text (disabled) +pub const BUTTON_TEXT_DISABLED: &str = "feathers.button.txt.disabled"; + +// Primary ("default") buttons + +/// Primary button background +pub const BUTTON_PRIMARY_BG: &str = "feathers.button.primary.bg"; +/// Primary button background (hovered) +pub const BUTTON_PRIMARY_BG_HOVER: &str = "feathers.button.primary.bg.hover"; +/// Primary button background (disabled) +pub const BUTTON_PRIMARY_BG_DISABLED: &str = "feathers.button.primary.bg.disabled"; +/// Primary button background (pressed) +pub const BUTTON_PRIMARY_BG_PRESSED: &str = "feathers.button.primary.bg.pressed"; +/// Primary button text +pub const BUTTON_PRIMARY_TEXT: &str = "feathers.button.primary.txt"; +/// Primary button text (disabled) +pub const BUTTON_PRIMARY_TEXT_DISABLED: &str = "feathers.button.primary.txt.disabled"; + +// Slider + +/// Background for slider +pub const SLIDER_BG: &str = "feathers.slider.bg"; +/// Background for slider moving bar +pub const SLIDER_BAR: &str = "feathers.slider.bar"; +/// Background for slider moving bar (disabled) +pub const SLIDER_BAR_DISABLED: &str = "feathers.slider.bar.disabled"; +/// Background for slider text +pub const SLIDER_TEXT: &str = "feathers.slider.text"; +/// Background for slider text (disabled) +pub const SLIDER_TEXT_DISABLED: &str = "feathers.slider.text.disabled"; + +// Checkbox + +/// Checkbox background around the checkmark +pub const CHECKBOX_BG: &str = "feathers.checkbox.bg"; +/// Checkbox border around the checkmark (disabled) +pub const CHECKBOX_BG_DISABLED: &str = "feathers.checkbox.bg.disabled"; +/// Checkbox background around the checkmark +pub const CHECKBOX_BG_CHECKED: &str = "feathers.checkbox.bg.checked"; +/// Checkbox border around the checkmark (disabled) +pub const CHECKBOX_BG_CHECKED_DISABLED: &str = "feathers.checkbox.bg.checked.disabled"; +/// Checkbox border around the checkmark +pub const CHECKBOX_BORDER: &str = "feathers.checkbox.border"; +/// Checkbox border around the checkmark (hovered) +pub const CHECKBOX_BORDER_HOVER: &str = "feathers.checkbox.border.hover"; +/// Checkbox border around the checkmark (disabled) +pub const CHECKBOX_BORDER_DISABLED: &str = "feathers.checkbox.border.disabled"; +/// Checkbox check mark +pub const CHECKBOX_MARK: &str = "feathers.checkbox.mark"; +/// Checkbox check mark (disabled) +pub const CHECKBOX_MARK_DISABLED: &str = "feathers.checkbox.mark.disabled"; +/// Checkbox label text +pub const CHECKBOX_TEXT: &str = "feathers.checkbox.text"; +/// Checkbox label text (disabled) +pub const CHECKBOX_TEXT_DISABLED: &str = "feathers.checkbox.text.disabled"; + +// Radio button + +/// Radio border around the checkmark +pub const RADIO_BORDER: &str = "feathers.radio.border"; +/// Radio border around the checkmark (hovered) +pub const RADIO_BORDER_HOVER: &str = "feathers.radio.border.hover"; +/// Radio border around the checkmark (disabled) +pub const RADIO_BORDER_DISABLED: &str = "feathers.radio.border.disabled"; +/// Radio check mark +pub const RADIO_MARK: &str = "feathers.radio.mark"; +/// Radio check mark (disabled) +pub const RADIO_MARK_DISABLED: &str = "feathers.radio.mark.disabled"; +/// Radio label text +pub const RADIO_TEXT: &str = "feathers.radio.text"; +/// Radio label text (disabled) +pub const RADIO_TEXT_DISABLED: &str = "feathers.radio.text.disabled"; + +// Toggle Switch + +/// Switch background around the checkmark +pub const SWITCH_BG: &str = "feathers.switch.bg"; +/// Switch border around the checkmark (disabled) +pub const SWITCH_BG_DISABLED: &str = "feathers.switch.bg.disabled"; +/// Switch background around the checkmark +pub const SWITCH_BG_CHECKED: &str = "feathers.switch.bg.checked"; +/// Switch border around the checkmark (disabled) +pub const SWITCH_BG_CHECKED_DISABLED: &str = "feathers.switch.bg.checked.disabled"; +/// Switch border around the checkmark +pub const SWITCH_BORDER: &str = "feathers.switch.border"; +/// Switch border around the checkmark (hovered) +pub const SWITCH_BORDER_HOVER: &str = "feathers.switch.border.hover"; +/// Switch border around the checkmark (disabled) +pub const SWITCH_BORDER_DISABLED: &str = "feathers.switch.border.disabled"; +/// Switch slide +pub const SWITCH_SLIDE: &str = "feathers.switch.slide"; +/// Switch slide (disabled) +pub const SWITCH_SLIDE_DISABLED: &str = "feathers.switch.slide.disabled"; diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index 864df285d9..7effc016c4 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -1,21 +1,20 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" 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_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 b9f1d9d286..7ec1c2e93b 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Systems and type definitions for gamepad handling in Bevy. @@ -15,7 +15,7 @@ mod gilrs_system; mod rumble; #[cfg(not(target_arch = "wasm32"))] -use bevy_utils::synccell::SyncCell; +use bevy_platform::cell::SyncCell; #[cfg(target_arch = "wasm32")] use core::cell::RefCell; @@ -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_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs index 8f41a3ca22..b03fa69fe3 100644 --- a/crates/bevy_gilrs/src/rumble.rs +++ b/crates/bevy_gilrs/src/rumble.rs @@ -2,9 +2,9 @@ use crate::{Gilrs, GilrsGamepads}; use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource}; use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest}; +use bevy_platform::cell::SyncCell; use bevy_platform::collections::HashMap; use bevy_time::{Real, Time}; -use bevy_utils::synccell::SyncCell; use core::time::Duration; use gilrs::{ ff::{self, BaseEffect, BaseEffectType, Repeat, Replay}, diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 3a264c6244..c4833dbe7e 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_gizmos" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides gizmos for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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 b38a3c5374..f4273f05ed 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -13,10 +13,9 @@ 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" quote = "1.0" [lints] diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 06a6a71f1f..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,21 +200,24 @@ 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), } } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &bevy_ecs::archetype::Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { - GizmosState::::new_archetype(&mut state.state, archetype, system_meta); - }; + GizmosState::::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -222,12 +226,14 @@ where #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { // SAFETY: Delegated to existing `SystemParam` implementations. - unsafe { GizmosState::::validate_param(&state.state, system_meta, world) } + unsafe { + GizmosState::::validate_param(&mut state.state, system_meta, world) + } } #[inline] 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 581a30091d..4eeeea508d 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! This crate adds an immediate mode drawing api to Bevy for visual debugging. @@ -102,7 +102,7 @@ use crate::{config::ErasedGizmoConfigGroup, gizmos::GizmoBuffer}; #[cfg(feature = "bevy_render")] use { crate::retained::extract_linegizmos, - bevy_asset::{weak_handle, AssetId}, + bevy_asset::AssetId, bevy_ecs::{ component::Component, entity::Entity, @@ -119,12 +119,12 @@ use { render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::{ binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout, - BindGroupLayoutEntries, Buffer, BufferInitDescriptor, BufferUsages, Shader, - ShaderStages, ShaderType, VertexFormat, + BindGroupLayoutEntries, Buffer, BufferInitDescriptor, BufferUsages, ShaderStages, + ShaderType, VertexFormat, }, renderer::RenderDevice, sync_world::{MainEntity, TemporaryRenderEntity}, - Extract, ExtractSchedule, Render, RenderApp, RenderSystems, + Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, }, bytemuck::cast_slice, }; @@ -144,12 +144,6 @@ use gizmos::{GizmoStorage, Swap}; #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] use light::LightGizmoPlugin; -#[cfg(feature = "bevy_render")] -const LINE_SHADER_HANDLE: Handle = weak_handle!("15dc5869-ad30-4664-b35a-4137cb8804a1"); -#[cfg(feature = "bevy_render")] -const LINE_JOINT_SHADER_HANDLE: Handle = - weak_handle!("7b5bdda5-df81-4711-a6cf-e587700de6f2"); - /// A [`Plugin`] that provides an immediate mode drawing api for visual debugging. /// /// Requires to be loaded after [`PbrPlugin`](bevy_pbr::PbrPlugin) or [`SpritePlugin`](bevy_sprite::SpritePlugin). @@ -160,14 +154,9 @@ impl Plugin for GizmoPlugin { fn build(&self, app: &mut App) { #[cfg(feature = "bevy_render")] { - use bevy_asset::load_internal_asset; - load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - LINE_JOINT_SHADER_HANDLE, - "line_joints.wgsl", - Shader::from_wgsl - ); + use bevy_asset::embedded_asset; + embedded_asset!(app, "lines.wgsl"); + embedded_asset!(app, "line_joints.wgsl"); } app.register_type::() @@ -187,6 +176,8 @@ impl Plugin for GizmoPlugin { #[cfg(feature = "bevy_render")] if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems(RenderStartup, init_line_gizmo_uniform_bind_group_layout); + render_app.add_systems( Render, prepare_line_gizmo_bind_group.in_set(RenderSystems::PrepareBindGroups), @@ -210,26 +201,6 @@ impl Plugin for GizmoPlugin { tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); } } - - #[cfg(feature = "bevy_render")] - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - let render_device = render_app.world().resource::(); - let line_layout = render_device.create_bind_group_layout( - "LineGizmoUniform layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX, - uniform_buffer::(true), - ), - ); - - render_app.insert_resource(LineGizmoUniformBindgroupLayout { - layout: line_layout, - }); - } } /// A extension trait adding `App::init_gizmo_group` and `App::insert_gizmo_config`. @@ -426,6 +397,24 @@ fn update_gizmo_meshes( } } +#[cfg(feature = "bevy_render")] +fn init_line_gizmo_uniform_bind_group_layout( + mut commands: Commands, + render_device: Res, +) { + let line_layout = render_device.create_bind_group_layout( + "LineGizmoUniform layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX, + uniform_buffer::(true), + ), + ); + + commands.insert_resource(LineGizmoUniformBindgroupLayout { + layout: line_layout, + }); +} + #[cfg(feature = "bevy_render")] fn extract_gizmo_data( mut commands: Commands, @@ -565,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, @@ -642,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 { @@ -673,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 { @@ -736,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_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 15ed1c3ab0..128ecca883 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,22 +1,21 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, - line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, - SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, + init_line_gizmo_uniform_bind_group_layout, line_gizmo_vertex_buffer_layouts, + line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems, + GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, }; use bevy_app::{App, Plugin}; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, resource::Resource, schedule::IntoScheduleConfigs, - system::{Query, Res, ResMut}, - world::{FromWorld, World}, + system::{Commands, Query, Res, ResMut}, }; use bevy_image::BevyDefault as _; use bevy_math::FloatOrd; -use bevy_render::sync_world::MainEntity; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, render_phase::{ @@ -27,7 +26,9 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSystems, }; +use bevy_render::{sync_world::MainEntity, RenderStartup}; use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; +use bevy_utils::default; use tracing::error; pub struct LineGizmo2dPlugin; @@ -53,6 +54,10 @@ impl Plugin for LineGizmo2dPlugin { bevy_sprite::queue_material2d_meshes::, ), ) + .add_systems( + RenderStartup, + init_line_gizmo_pipelines.after(init_line_gizmo_uniform_bind_group_layout), + ) .add_systems( Render, (queue_line_gizmos_2d, queue_line_joint_gizmos_2d) @@ -60,33 +65,31 @@ impl Plugin for LineGizmo2dPlugin { .after(prepare_assets::), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app.init_resource::(); - render_app.init_resource::(); - } } #[derive(Clone, Resource)] struct LineGizmoPipeline { mesh_pipeline: Mesh2dPipeline, uniform_layout: BindGroupLayout, + shader: Handle, } -impl FromWorld for LineGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - } - } +fn init_line_gizmo_pipelines( + mut commands: Commands, + mesh_2d_pipeline: Res, + uniform_bind_group_layout: Res, + asset_server: Res, +) { + commands.insert_resource(LineGizmoPipeline { + mesh_pipeline: mesh_2d_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "lines.wgsl"), + }); + commands.insert_resource(LineJointGizmoPipeline { + mesh_pipeline: mesh_2d_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "line_joints.wgsl"), + }); } #[derive(PartialEq, Eq, Hash, Clone)] @@ -124,15 +127,15 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.shader.clone(), shader_defs: shader_defs.clone(), buffers: line_gizmo_vertex_buffer_layouts(key.strip), + ..default() }, fragment: Some(FragmentState { - shader: LINE_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: fragment_entry_point.into(), + entry_point: Some(fragment_entry_point.into()), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), @@ -140,7 +143,6 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { })], }), layout, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_2D_DEPTH_FORMAT, depth_write_enabled: false, @@ -163,8 +165,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineGizmo Pipeline 2D".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -173,18 +174,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { struct LineJointGizmoPipeline { mesh_pipeline: Mesh2dPipeline, uniform_layout: BindGroupLayout, -} - -impl FromWorld for LineJointGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineJointGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - } - } + shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -225,20 +215,20 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_JOINT_SHADER_HANDLE, - entry_point: entry_point.into(), + shader: self.shader.clone(), + entry_point: Some(entry_point.into()), shader_defs: shader_defs.clone(), buffers: line_joint_gizmo_vertex_buffer_layouts(), }, fragment: Some(FragmentState { - shader: LINE_JOINT_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout, primitive: PrimitiveState::default(), @@ -264,8 +254,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineJointGizmo Pipeline 2D".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 87d865f8ca..66f2050e55 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,10 +1,11 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, - line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, - SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, + init_line_gizmo_uniform_bind_group_layout, line_gizmo_vertex_buffer_layouts, + line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems, + GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, }; use bevy_app::{App, Plugin}; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::{ core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}, oit::OrderIndependentTransparencySettings, @@ -16,12 +17,10 @@ use bevy_ecs::{ query::Has, resource::Resource, schedule::IntoScheduleConfigs, - system::{Query, Res, ResMut}, - world::{FromWorld, World}, + system::{Commands, Query, Res, ResMut}, }; use bevy_image::BevyDefault as _; use bevy_pbr::{MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup}; -use bevy_render::sync_world::MainEntity; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, render_phase::{ @@ -32,6 +31,8 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSystems, }; +use bevy_render::{sync_world::MainEntity, RenderStartup}; +use bevy_utils::default; use tracing::error; pub struct LineGizmo3dPlugin; @@ -49,9 +50,11 @@ impl Plugin for LineGizmo3dPlugin { .init_resource::>() .configure_sets( Render, - GizmoRenderSystems::QueueLineGizmos3d - .in_set(RenderSystems::Queue) - .ambiguous_with(bevy_pbr::queue_material_meshes::), + GizmoRenderSystems::QueueLineGizmos3d.in_set(RenderSystems::Queue), + ) + .add_systems( + RenderStartup, + init_line_gizmo_pipelines.after(init_line_gizmo_uniform_bind_group_layout), ) .add_systems( Render, @@ -60,33 +63,31 @@ impl Plugin for LineGizmo3dPlugin { .after(prepare_assets::), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app.init_resource::(); - render_app.init_resource::(); - } } #[derive(Clone, Resource)] struct LineGizmoPipeline { mesh_pipeline: MeshPipeline, uniform_layout: BindGroupLayout, + shader: Handle, } -impl FromWorld for LineGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - } - } +fn init_line_gizmo_pipelines( + mut commands: Commands, + mesh_pipeline: Res, + uniform_bind_group_layout: Res, + asset_server: Res, +) { + commands.insert_resource(LineGizmoPipeline { + mesh_pipeline: mesh_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "lines.wgsl"), + }); + commands.insert_resource(LineJointGizmoPipeline { + mesh_pipeline: mesh_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "line_joints.wgsl"), + }); } #[derive(PartialEq, Eq, Hash, Clone)] @@ -120,8 +121,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", @@ -131,15 +131,15 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.shader.clone(), shader_defs: shader_defs.clone(), buffers: line_gizmo_vertex_buffer_layouts(key.strip), + ..default() }, fragment: Some(FragmentState { - shader: LINE_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: fragment_entry_point.into(), + entry_point: Some(fragment_entry_point.into()), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), @@ -147,7 +147,6 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { })], }), layout, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -161,8 +160,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineGizmo 3d Pipeline".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -171,18 +169,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { struct LineJointGizmoPipeline { mesh_pipeline: MeshPipeline, uniform_layout: BindGroupLayout, -} - -impl FromWorld for LineJointGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineJointGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - } - } + shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -215,8 +202,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."); @@ -230,23 +216,22 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_JOINT_SHADER_HANDLE, - entry_point: entry_point.into(), + shader: self.shader.clone(), + entry_point: Some(entry_point.into()), shader_defs: shader_defs.clone(), buffers: line_joint_gizmo_vertex_buffer_layouts(), }, fragment: Some(FragmentState { - shader: LINE_JOINT_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -260,8 +245,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineJointGizmo 3d Pipeline".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 88610b9744..4cc75f236d 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -149,7 +149,7 @@ pub(crate) fn extract_linegizmos( line_style: gizmo.line_config.style, line_joints: gizmo.line_config.joints, render_layers: render_layers.cloned().unwrap_or_default(), - handle: gizmo.handle.clone_weak(), + handle: gizmo.handle.clone(), }, MainEntity::from(entity), TemporaryRenderEntity, diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index a67ab2276c..7c4216d889 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_gltf" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy Engine GLTF loading" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -15,28 +15,28 @@ pbr_multi_layer_material_textures = [ ] pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"] pbr_specular_textures = ["bevy_pbr/pbr_specular_textures"] +gltf_convert_coordinates_default = [] [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_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -61,12 +61,10 @@ fixedbitset = "0.5" itertools = "0.14" percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } -serde_json = "1" -smallvec = "1.11" +serde_json = "1.0.140" +smallvec = { version = "1", default-features = false } 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/assets.rs b/crates/bevy_gltf/src/assets.rs index fe3303dd81..bfc920ebce 100644 --- a/crates/bevy_gltf/src/assets.rs +++ b/crates/bevy_gltf/src/assets.rs @@ -1,5 +1,7 @@ //! Representation of assets present in a glTF file +use core::ops::Deref; + #[cfg(feature = "bevy_animation")] use bevy_animation::AnimationClip; use bevy_asset::{Asset, Handle}; @@ -297,6 +299,21 @@ pub struct GltfMeshExtras { pub value: String, } +/// The mesh name of a glTF primitive. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh). +#[derive(Clone, Debug, Reflect, Default, Component)] +#[reflect(Component, Clone)] +pub struct GltfMeshName(pub String); + +impl Deref for GltfMeshName { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + /// Additional untyped data that can be present on most glTF types at the material level. /// /// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). @@ -313,3 +330,11 @@ pub struct GltfMaterialExtras { #[derive(Clone, Debug, Reflect, Default, Component)] #[reflect(Component, Clone)] pub struct GltfMaterialName(pub String); + +impl Deref for GltfMaterialName { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} 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 02c14f4197..6b90a4d99b 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Plugin providing an [`AssetLoader`](bevy_asset::AssetLoader) and type definitions @@ -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; @@ -99,15 +100,15 @@ extern crate alloc; use alloc::sync::Arc; use std::sync::Mutex; +use tracing::warn; use bevy_platform::collections::HashMap; use bevy_app::prelude::*; use bevy_asset::AssetApp; use bevy_ecs::prelude::Resource; -use bevy_image::{CompressedImageFormats, ImageSamplerDescriptor}; +use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageSamplerDescriptor}; use bevy_mesh::MeshVertexAttribute; -use bevy_render::renderer::RenderDevice; /// The glTF prelude. /// @@ -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: cfg!(feature = "gltf_convert_coordinates_default"), } } } @@ -193,6 +211,7 @@ impl Plugin for GltfPlugin { app.register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .init_asset::() @@ -204,17 +223,25 @@ impl Plugin for GltfPlugin { } fn finish(&self, app: &mut App) { - let supported_compressed_formats = match app.world().get_resource::() { - Some(render_device) => CompressedImageFormats::from_features(render_device.features()), - None => CompressedImageFormats::NONE, + let supported_compressed_formats = if let Some(resource) = + app.world().get_resource::() + { + resource.0 + } else { + warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ + RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); + CompressedImageFormats::NONE }; + 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 ef719891a4..25d6677832 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs @@ -1,13 +1,17 @@ use bevy_mesh::PrimitiveTopology; -use gltf::mesh::{Mesh, Mode, Primitive}; +use gltf::{ + mesh::{Mesh, Mode}, + Material, +}; use crate::GltfError; -pub(crate) fn primitive_name(mesh: &Mesh<'_>, primitive: &Primitive) -> String { +pub(crate) fn primitive_name(mesh: &Mesh<'_>, material: &Material) -> String { let mesh_name = mesh.name().unwrap_or("Mesh"); - if mesh.primitives().len() > 1 { - format!("{}.{}", mesh_name, primitive.index()) + + if let Some(material_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/gltf_ext/texture.rs b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs index f666752479..0ea16936a6 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/texture.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs @@ -51,7 +51,7 @@ pub(crate) fn texture_sampler( // Shouldn't parse filters when anisotropic filtering is on, because trilinear is then required by wgpu. // We also trust user to have provided a valid sampler. - if sampler.anisotropy_clamp != 1 { + if sampler.anisotropy_clamp == 1 { if let Some(mag_filter) = gltf_sampler.mag_filter().map(|mf| match mf { MagFilter::Nearest => ImageFilterMode::Nearest, MagFilter::Linear => ImageFilterMode::Linear, diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index f85a739b2e..3e4c384532 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -2,6 +2,7 @@ mod extensions; mod gltf_ext; use alloc::sync::Arc; +use bevy_log::warn_once; use std::{ io::Error, path::{Path, PathBuf}, @@ -66,7 +67,7 @@ use tracing::{error, info_span, warn}; use crate::{ vertex_attributes::convert_attribute, Gltf, GltfAssetLabel, GltfExtras, GltfMaterialExtras, - GltfMaterialName, GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin, + GltfMaterialName, GltfMeshExtras, GltfMeshName, GltfNode, GltfSceneExtras, GltfSkin, }; #[cfg(feature = "bevy_animation")] @@ -84,6 +85,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 +152,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 +203,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 +236,7 @@ impl Default for GltfLoaderSettings { include_source: false, default_sampler: None, override_sampler: false, + convert_coordinates: None, } } } @@ -262,6 +296,22 @@ async fn load_gltf<'a, 'b, 'c>( paths }; + let convert_coordinates = match settings.convert_coordinates { + Some(convert_coordinates) => convert_coordinates, + None => { + let convert_by_default = loader.default_convert_coordinates; + if !convert_by_default && !cfg!(feature = "gltf_convert_coordinates_default") { + warn_once!( + "Starting from Bevy 0.18, by default all imported glTF models will be rotated by 180 degrees around the Y axis to align with Bevy's coordinate system. \ + You are currently importing glTF files using the old behavior. Consider opting-in to the new import behavior by enabling the `gltf_convert_coordinates_default` feature. \ + If you encounter any issues please file a bug! \ + If you want to continue using the old behavior going forward (even when the default changes in 0.18), manually set the corresponding option in the `GltfPlugin` or `GltfLoaderSettings`. See the migration guide for more details." + ); + } + convert_by_default + } + }; + #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { use bevy_animation::{animated_field, animation_curves::*, gltf_curves::*, VariableCurve}; @@ -303,7 +353,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 +409,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 +701,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 +821,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 +913,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 +964,7 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] None, &gltf.document, + convert_coordinates, ); if result.is_err() { err = Some(result); @@ -1043,71 +1123,75 @@ fn load_material( is_scale_inverted: bool, ) -> Handle { let material_label = material_label(material, is_scale_inverted); - load_context.labeled_asset_scope(material_label.to_string(), |load_context| { - let pbr = material.pbr_metallic_roughness(); + load_context + .labeled_asset_scope::<_, ()>(material_label.to_string(), |load_context| { + let pbr = material.pbr_metallic_roughness(); - // TODO: handle missing label handle errors here? - let color = pbr.base_color_factor(); - let base_color_channel = pbr - .base_color_texture() - .map(|info| uv_channel(material, "base color", info.tex_coord())) - .unwrap_or_default(); - let base_color_texture = pbr - .base_color_texture() - .map(|info| texture_handle(&info.texture(), load_context)); + // TODO: handle missing label handle errors here? + let color = pbr.base_color_factor(); + let base_color_channel = pbr + .base_color_texture() + .map(|info| uv_channel(material, "base color", info.tex_coord())) + .unwrap_or_default(); + let base_color_texture = pbr + .base_color_texture() + .map(|info| texture_handle(&info.texture(), load_context)); - let uv_transform = pbr - .base_color_texture() - .and_then(|info| info.texture_transform().map(texture_transform_to_affine2)) - .unwrap_or_default(); + let uv_transform = pbr + .base_color_texture() + .and_then(|info| info.texture_transform().map(texture_transform_to_affine2)) + .unwrap_or_default(); - let normal_map_channel = material - .normal_texture() - .map(|info| uv_channel(material, "normal map", info.tex_coord())) - .unwrap_or_default(); - let normal_map_texture: Option> = - material.normal_texture().map(|normal_texture| { - // TODO: handle normal_texture.scale - texture_handle(&normal_texture.texture(), load_context) + let normal_map_channel = material + .normal_texture() + .map(|info| uv_channel(material, "normal map", info.tex_coord())) + .unwrap_or_default(); + let normal_map_texture: Option> = + material.normal_texture().map(|normal_texture| { + // TODO: handle normal_texture.scale + texture_handle(&normal_texture.texture(), load_context) + }); + + let metallic_roughness_channel = pbr + .metallic_roughness_texture() + .map(|info| uv_channel(material, "metallic/roughness", info.tex_coord())) + .unwrap_or_default(); + let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { + warn_on_differing_texture_transforms( + material, + &info, + uv_transform, + "metallic/roughness", + ); + texture_handle(&info.texture(), load_context) }); - let metallic_roughness_channel = pbr - .metallic_roughness_texture() - .map(|info| uv_channel(material, "metallic/roughness", info.tex_coord())) - .unwrap_or_default(); - let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { - warn_on_differing_texture_transforms( - material, - &info, - uv_transform, - "metallic/roughness", - ); - texture_handle(&info.texture(), load_context) - }); + let occlusion_channel = material + .occlusion_texture() + .map(|info| uv_channel(material, "occlusion", info.tex_coord())) + .unwrap_or_default(); + let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { + // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) + texture_handle(&occlusion_texture.texture(), load_context) + }); - let occlusion_channel = material - .occlusion_texture() - .map(|info| uv_channel(material, "occlusion", info.tex_coord())) - .unwrap_or_default(); - let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { - // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) - texture_handle(&occlusion_texture.texture(), load_context) - }); + let emissive = material.emissive_factor(); + let emissive_channel = material + .emissive_texture() + .map(|info| uv_channel(material, "emissive", info.tex_coord())) + .unwrap_or_default(); + let emissive_texture = material.emissive_texture().map(|info| { + // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) + warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); + texture_handle(&info.texture(), load_context) + }); - let emissive = material.emissive_factor(); - let emissive_channel = material - .emissive_texture() - .map(|info| uv_channel(material, "emissive", info.tex_coord())) - .unwrap_or_default(); - let emissive_texture = material.emissive_texture().map(|info| { - // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) - warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); - texture_handle(&info.texture(), load_context) - }); - - #[cfg(feature = "pbr_transmission_textures")] - let (specular_transmission, specular_transmission_channel, specular_transmission_texture) = - material + #[cfg(feature = "pbr_transmission_textures")] + let ( + specular_transmission, + specular_transmission_channel, + specular_transmission_texture, + ) = material .transmission() .map_or((0.0, UvChannel::Uv0, None), |transmission| { let specular_transmission_channel = transmission @@ -1127,152 +1211,156 @@ fn load_material( ) }); - #[cfg(not(feature = "pbr_transmission_textures"))] - let specular_transmission = material - .transmission() - .map_or(0.0, |transmission| transmission.transmission_factor()); + #[cfg(not(feature = "pbr_transmission_textures"))] + let specular_transmission = material + .transmission() + .map_or(0.0, |transmission| transmission.transmission_factor()); - #[cfg(feature = "pbr_transmission_textures")] - let ( - thickness, - thickness_channel, - thickness_texture, - attenuation_distance, - attenuation_color, - ) = material.volume().map_or( - (0.0, UvChannel::Uv0, None, f32::INFINITY, [1.0, 1.0, 1.0]), - |volume| { - let thickness_channel = volume - .thickness_texture() - .map(|info| uv_channel(material, "thickness", info.tex_coord())) - .unwrap_or_default(); - let thickness_texture: Option> = - volume.thickness_texture().map(|thickness_texture| { - texture_handle(&thickness_texture.texture(), load_context) - }); + #[cfg(feature = "pbr_transmission_textures")] + let ( + thickness, + thickness_channel, + thickness_texture, + attenuation_distance, + attenuation_color, + ) = material.volume().map_or( + (0.0, UvChannel::Uv0, None, f32::INFINITY, [1.0, 1.0, 1.0]), + |volume| { + let thickness_channel = volume + .thickness_texture() + .map(|info| uv_channel(material, "thickness", info.tex_coord())) + .unwrap_or_default(); + let thickness_texture: Option> = + volume.thickness_texture().map(|thickness_texture| { + texture_handle(&thickness_texture.texture(), load_context) + }); - ( - volume.thickness_factor(), - thickness_channel, - thickness_texture, - volume.attenuation_distance(), - volume.attenuation_color(), - ) - }, - ); - - #[cfg(not(feature = "pbr_transmission_textures"))] - let (thickness, attenuation_distance, attenuation_color) = - material - .volume() - .map_or((0.0, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { ( volume.thickness_factor(), + thickness_channel, + thickness_texture, volume.attenuation_distance(), volume.attenuation_color(), ) - }); + }, + ); - let ior = material.ior().unwrap_or(1.5); + #[cfg(not(feature = "pbr_transmission_textures"))] + let (thickness, attenuation_distance, attenuation_color) = + material + .volume() + .map_or((0.0, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { + ( + volume.thickness_factor(), + volume.attenuation_distance(), + volume.attenuation_color(), + ) + }); - // Parse the `KHR_materials_clearcoat` extension data if necessary. - let clearcoat = - ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); + let ior = material.ior().unwrap_or(1.5); - // Parse the `KHR_materials_anisotropy` extension data if necessary. - let anisotropy = - AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_clearcoat` extension data if necessary. + let clearcoat = + ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); - // Parse the `KHR_materials_specular` extension data if necessary. - let specular = - SpecularExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_anisotropy` extension data if necessary. + let anisotropy = + AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); - // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels - let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); - let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); + // Parse the `KHR_materials_specular` extension data if necessary. + let specular = + SpecularExtension::parse(load_context, document, material).unwrap_or_default(); - StandardMaterial { - base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), - base_color_channel, - base_color_texture, - perceptual_roughness: pbr.roughness_factor(), - metallic: pbr.metallic_factor(), - metallic_roughness_channel, - metallic_roughness_texture, - normal_map_channel, - normal_map_texture, - double_sided: material.double_sided(), - cull_mode: if material.double_sided() { - None - } else if is_scale_inverted { - Some(Face::Front) - } else { - Some(Face::Back) - }, - occlusion_channel, - occlusion_texture, - emissive, - emissive_channel, - emissive_texture, - specular_transmission, - #[cfg(feature = "pbr_transmission_textures")] - specular_transmission_channel, - #[cfg(feature = "pbr_transmission_textures")] - specular_transmission_texture, - thickness, - #[cfg(feature = "pbr_transmission_textures")] - thickness_channel, - #[cfg(feature = "pbr_transmission_textures")] - thickness_texture, - ior, - attenuation_distance, - attenuation_color: Color::linear_rgb( - attenuation_color[0], - attenuation_color[1], - attenuation_color[2], - ), - unlit: material.unlit(), - alpha_mode: alpha_mode(material), - uv_transform, - clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, - clearcoat_perceptual_roughness: clearcoat.clearcoat_roughness_factor.unwrap_or_default() - as f32, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_channel: clearcoat.clearcoat_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_texture: clearcoat.clearcoat_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_channel: clearcoat.clearcoat_roughness_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, - anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32, - anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_channel: anisotropy.anisotropy_channel, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_texture: anisotropy.anisotropy_texture, - // From the `KHR_materials_specular` spec: - // - reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, - #[cfg(feature = "pbr_specular_textures")] - specular_channel: specular.specular_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_texture: specular.specular_texture, - specular_tint: match specular.specular_color_factor { - Some(color) => Color::linear_rgb(color[0] as f32, color[1] as f32, color[2] as f32), - None => Color::WHITE, - }, - #[cfg(feature = "pbr_specular_textures")] - specular_tint_channel: specular.specular_color_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_tint_texture: specular.specular_color_texture, - ..Default::default() - } - }) + // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels + let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); + let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); + + Ok(StandardMaterial { + base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), + base_color_channel, + base_color_texture, + perceptual_roughness: pbr.roughness_factor(), + metallic: pbr.metallic_factor(), + metallic_roughness_channel, + metallic_roughness_texture, + normal_map_channel, + normal_map_texture, + double_sided: material.double_sided(), + cull_mode: if material.double_sided() { + None + } else if is_scale_inverted { + Some(Face::Front) + } else { + Some(Face::Back) + }, + occlusion_channel, + occlusion_texture, + emissive, + emissive_channel, + emissive_texture, + specular_transmission, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_channel, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_texture, + thickness, + #[cfg(feature = "pbr_transmission_textures")] + thickness_channel, + #[cfg(feature = "pbr_transmission_textures")] + thickness_texture, + ior, + attenuation_distance, + attenuation_color: Color::linear_rgb( + attenuation_color[0], + attenuation_color[1], + attenuation_color[2], + ), + unlit: material.unlit(), + alpha_mode: alpha_mode(material), + uv_transform, + clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, + clearcoat_perceptual_roughness: clearcoat + .clearcoat_roughness_factor + .unwrap_or_default() as f32, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: clearcoat.clearcoat_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: clearcoat.clearcoat_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: clearcoat.clearcoat_roughness_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, + anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32, + anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_channel: anisotropy.anisotropy_channel, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_texture: anisotropy.anisotropy_texture, + // From the `KHR_materials_specular` spec: + // + reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: specular.specular_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: specular.specular_texture, + specular_tint: match specular.specular_color_factor { + Some(color) => { + Color::linear_rgb(color[0] as f32, color[1] as f32, color[2] as f32) + } + None => Color::WHITE, + }, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_channel: specular.specular_color_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_texture: specular.specular_color_texture, + ..Default::default() + }) + }) + .unwrap() } /// Loads a glTF node. @@ -1296,9 +1384,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 @@ -1351,7 +1440,6 @@ fn load_node( }, ..OrthographicProjection::default_3d() }; - Projection::Orthographic(orthographic_projection) } gltf::camera::Projection::Perspective(perspective) => { @@ -1369,6 +1457,7 @@ fn load_node( Projection::Perspective(perspective_projection) } }; + node.insert(( Camera3d::default(), projection, @@ -1463,11 +1552,16 @@ fn load_node( }); } - if let Some(name) = material.name() { - mesh_entity.insert(GltfMaterialName(String::from(name))); + if let Some(name) = mesh.name() { + mesh_entity.insert(GltfMeshName(name.to_string())); } - mesh_entity.insert(Name::new(primitive_name(&mesh, &primitive))); + if let Some(name) = material.name() { + mesh_entity.insert(GltfMaterialName(name.to_string())); + } + + mesh_entity.insert(Name::new(primitive_name(&mesh, &material))); + // Mark for adding skinned mesh if let Some(skin) = gltf_node.skin() { entity_to_skin_index_map.insert(mesh_entity.id(), skin.index()); @@ -1562,6 +1656,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 988325c707..7b49b5210a 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -32,24 +32,35 @@ qoi = ["image/qoi"] tga = ["image/tga"] tiff = ["image/tiff"] webp = ["image/webp"] -serialize = ["bevy_reflect", "bevy_platform/serialize", "bevy_utils/serde"] +serialize = ["bevy_reflect", "bevy_platform/serialize"] # For ktx2 supercompression zlib = ["flate2"] -zstd = ["ruzstd"] + +# A marker feature indicating zstd support is required for a particular feature. +# A backend must be chosen by enabling either the "zstd_rust" or the "zstd_c" feature. +zstd = [] +# Pure-rust zstd implementation (safer) +zstd_rust = ["zstd", "dep:ruzstd"] +# Binding to zstd C implementation (faster) +zstd_c = ["zstd", "dep:zstd"] + +# 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_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", ] } @@ -59,7 +70,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" @@ -69,6 +80,7 @@ ddsfile = { version = "0.5.2", optional = true } ktx2 = { version = "0.4.0", optional = true } # For ktx2 supercompression flate2 = { version = "1.0.22", optional = true } +zstd = { version = "0.13.3", optional = true } ruzstd = { version = "0.8.0", optional = true } # For transcoding of UASTC/ETC1S universal formats, and for .basis file support basis-universal = { version = "0.3.0", optional = true } @@ -76,7 +88,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/dds.rs b/crates/bevy_image/src/dds.rs index 8dc58ad482..28bf637333 100644 --- a/crates/bevy_image/src/dds.rs +++ b/crates/bevy_image/src/dds.rs @@ -23,9 +23,9 @@ pub fn dds_buffer_to_image( Ok(format) => (format, None), Err(TextureError::FormatRequiresTranscodingError(TranscodeFormat::Rgb8)) => { let format = if is_srgb { - TextureFormat::Bgra8UnormSrgb + TextureFormat::Rgba8UnormSrgb } else { - TextureFormat::Bgra8Unorm + TextureFormat::Rgba8Unorm }; (format, Some(TranscodeFormat::Rgb8)) } diff --git a/crates/bevy_image/src/dynamic_texture_atlas_builder.rs b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs index e8b812194a..3e1c8fd542 100644 --- a/crates/bevy_image/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs @@ -5,8 +5,11 @@ use guillotiere::{size2, Allocation, AtlasAllocator}; use thiserror::Error; use tracing::error; +/// An error produced by [`DynamicTextureAtlasBuilder`] when trying to add a new +/// texture to a [`TextureAtlasLayout`]. #[derive(Debug, Error)] pub enum DynamicTextureAtlasBuilderError { + /// Unable to allocate space within the atlas for the new texture #[error("Couldn't allocate space to add the image requested")] FailedToAllocateSpace, /// Attempted to add a texture to an uninitialized atlas diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 5260c70bfc..195debc1d4 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -11,18 +11,21 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_asset::{Asset, RenderAssetUsages}; use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza}; +use bevy_ecs::resource::Resource; use bevy_math::{AspectRatio, UVec2, UVec3, Vec2}; use core::hash::Hash; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tracing::warn; use wgpu_types::{ AddressMode, CompareFunction, Extent3d, Features, FilterMode, SamplerBorderColor, - SamplerDescriptor, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, - TextureViewDescriptor, + SamplerDescriptor, TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, TextureViewDescriptor, }; +/// Trait used to provide default values for Bevy-external types that +/// do not implement [`Default`]. pub trait BevyDefault { + /// Returns the default value for a type. fn bevy_default() -> Self; } @@ -334,6 +337,28 @@ impl ImageFormat { } } +pub trait ToExtents { + fn to_extents(self) -> Extent3d; +} +impl ToExtents for UVec2 { + fn to_extents(self) -> Extent3d { + Extent3d { + width: self.x, + height: self.y, + depth_or_array_layers: 1, + } + } +} +impl ToExtents for UVec3 { + fn to_extents(self) -> Extent3d { + Extent3d { + width: self.x, + height: self.y, + depth_or_array_layers: self.z, + } + } +} + #[derive(Asset, Debug, Clone)] #[cfg_attr( feature = "bevy_reflect", @@ -347,18 +372,24 @@ pub struct Image { /// CPU, then this should be `None`. /// Otherwise, it should always be `Some`. pub data: Option>, + /// For texture data with layers and mips, this field controls how wgpu interprets the buffer layout. + /// + /// Use [`TextureDataOrder::default()`] for all other cases. + pub data_order: TextureDataOrder, // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors. pub texture_descriptor: TextureDescriptor, &'static [TextureFormat]>, /// The [`ImageSampler`] to use during rendering. 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, /// [`ImageSampler::Default`], will read the sampler from the `ImagePlugin` at setup. /// Setting this to [`ImageSampler::Descriptor`] will override the global default descriptor for this [`Image`]. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum ImageSampler { /// Default image sampler, derived from the `ImagePlugin` setup. #[default] @@ -403,7 +434,7 @@ impl ImageSampler { /// See [`ImageSamplerDescriptor`] for information how to configure this. /// /// This type mirrors [`AddressMode`]. -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum ImageAddressMode { /// Clamp the value to the edge of the texture. /// @@ -432,7 +463,7 @@ pub enum ImageAddressMode { /// Texel mixing mode when sampling between texels. /// /// This type mirrors [`FilterMode`]. -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum ImageFilterMode { /// Nearest neighbor sampling. /// @@ -448,7 +479,7 @@ pub enum ImageFilterMode { /// Comparison function used for depth and stencil operations. /// /// This type mirrors [`CompareFunction`]. -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum ImageCompareFunction { /// Function never passes Never, @@ -475,7 +506,7 @@ pub enum ImageCompareFunction { /// Color variation to use when the sampler addressing mode is [`ImageAddressMode::ClampToBorder`]. /// /// This type mirrors [`SamplerBorderColor`]. -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum ImageSamplerBorderColor { /// RGBA color `[0, 0, 0, 0]`. TransparentBlack, @@ -498,7 +529,7 @@ pub enum ImageSamplerBorderColor { /// a breaking change. /// /// This types mirrors [`SamplerDescriptor`], but that might change in future versions. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ImageSamplerDescriptor { pub label: Option, /// How to deal with out of bounds accesses in the u (i.e. x) direction. @@ -737,6 +768,7 @@ impl Image { ) -> Self { Image { data: None, + data_order: TextureDataOrder::default(), texture_descriptor: TextureDescriptor { size, format, @@ -744,12 +776,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, } } @@ -764,11 +799,7 @@ impl Image { debug_assert!(format.pixel_size() == 4); let data = vec![255, 255, 255, 0]; Image::new( - Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, + Extent3d::default(), TextureDimension::D2, data, format, @@ -778,11 +809,7 @@ impl Image { /// Creates a new uninitialized 1x1x1 image pub fn default_uninit() -> Image { Image::new_uninit( - Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, + Extent3d::default(), TextureDimension::D2, TextureFormat::bevy_default(), RenderAssetUsages::default(), @@ -810,8 +837,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) @@ -850,7 +876,9 @@ impl Image { } /// Resizes the image to the new size, by removing information or appending 0 to the `data`. - /// Does not properly resize the contents of the image, but only its internal `data` buffer. + /// Does not properly scale the contents of the image. + /// + /// If you need to keep pixel data intact, use [`Image::resize_in_place`]. pub fn resize(&mut self, size: Extent3d) { self.texture_descriptor.size = size; if let Some(ref mut data) = self.data { @@ -858,8 +886,6 @@ impl Image { size.volume() * self.texture_descriptor.format.pixel_size(), 0, ); - } else { - warn!("Resized an uninitialized image. Directly modify image.texture_descriptor.size instead"); } } @@ -880,6 +906,50 @@ impl Image { self.texture_descriptor.size = new_size; } + /// Resizes the image to the new size, keeping the pixel data intact, anchored at the top-left. + /// 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) { + 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 { + self.copy_on_resize = true; + return; + }; + + let mut new: Vec = vec![0; byte_len]; + + let copy_width = old_size.width.min(new_size.width) as usize; + let copy_height = old_size.height.min(new_size.height) as usize; + let copy_depth = old_size + .depth_or_array_layers + .min(new_size.depth_or_array_layers) as usize; + + let old_row_stride = old_size.width as usize * pixel_size; + let old_layer_stride = old_size.height as usize * old_row_stride; + + let new_row_stride = new_size.width as usize * pixel_size; + let new_layer_stride = new_size.height as usize * new_row_stride; + + for z in 0..copy_depth { + for y in 0..copy_height { + let old_offset = z * old_layer_stride + y * old_row_stride; + let new_offset = z * new_layer_stride + y * new_row_stride; + + let old_range = (old_offset)..(old_offset + copy_width * pixel_size); + let new_range = (new_offset)..(new_offset + copy_width * pixel_size); + + new[new_range].copy_from_slice(&data[old_range]); + } + } + + self.data = Some(new); + } + /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets /// it as a 2D array texture, where each of the stacked images becomes one layer of the /// array. This is primarily for use with the `texture2DArray` shader uniform type. @@ -1488,11 +1558,11 @@ pub enum DataFormat { pub enum TranscodeFormat { Etc1s, Uastc(DataFormat), - // Has to be transcoded to R8Unorm for use with `wgpu`. + /// Has to be transcoded from `R8UnormSrgb` to `R8Unorm` for use with `wgpu`. R8UnormSrgb, - // Has to be transcoded to R8G8Unorm for use with `wgpu`. + /// Has to be transcoded from `Rg8UnormSrgb` to `R8G8Unorm` for use with `wgpu`. Rg8UnormSrgb, - // Has to be transcoded to Rgba8 for use with `wgpu`. + /// Has to be transcoded from `Rgb8` to `Rgba8` for use with `wgpu`. Rgb8, } @@ -1651,6 +1721,12 @@ impl CompressedImageFormats { } } +/// For defining which compressed image formats are supported. This will be initialized from available device features +/// in `finish()` of the bevy `RenderPlugin`, but is left for the user to specify if not using the `RenderPlugin`, or +/// the WGPU backend. +#[derive(Resource)] +pub struct CompressedImageFormatSupport(pub CompressedImageFormats); + #[cfg(test)] mod test { use super::*; @@ -1726,4 +1802,163 @@ mod test { image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap(); assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE))); } + + #[test] + fn resize_in_place_2d_grow_and_shrink() { + use bevy_color::ColorToPacked; + + const INITIAL_FILL: LinearRgba = LinearRgba::BLACK; + const GROW_FILL: LinearRgba = LinearRgba::NONE; + + let mut image = Image::new_fill( + Extent3d { + width: 2, + height: 2, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &INITIAL_FILL.to_u8_array(), + TextureFormat::Rgba8Unorm, + RenderAssetUsages::MAIN_WORLD, + ); + + // Create a test pattern + + const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [ + (0, 1, LinearRgba::RED), + (1, 1, LinearRgba::GREEN), + (1, 0, LinearRgba::BLUE), + ]; + + for (x, y, color) in &TEST_PIXELS { + image.set_color_at(*x, *y, Color::from(*color)).unwrap(); + } + + // Grow image + 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!( + image.get_color_at(0, 0), + Ok(Color::LinearRgba(INITIAL_FILL)) + )); + for (x, y, color) in &TEST_PIXELS { + assert_eq!( + image.get_color_at(*x, *y).unwrap(), + Color::LinearRgba(*color) + ); + } + + // Pixels in the newly added area should get filled with zeroes. + assert!(matches!( + image.get_color_at(3, 3), + Ok(Color::LinearRgba(GROW_FILL)) + )); + + // Shrink + 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()); + } + + #[test] + fn resize_in_place_array_grow_and_shrink() { + use bevy_color::ColorToPacked; + + const INITIAL_FILL: LinearRgba = LinearRgba::BLACK; + const GROW_FILL: LinearRgba = LinearRgba::NONE; + const LAYERS: u32 = 4; + + let mut image = Image::new_fill( + Extent3d { + width: 2, + height: 2, + depth_or_array_layers: LAYERS, + }, + TextureDimension::D2, + &INITIAL_FILL.to_u8_array(), + TextureFormat::Rgba8Unorm, + RenderAssetUsages::MAIN_WORLD, + ); + + // Create a test pattern + + const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [ + (0, 1, LinearRgba::RED), + (1, 1, LinearRgba::GREEN), + (1, 0, LinearRgba::BLUE), + ]; + + for z in 0..LAYERS { + for (x, y, color) in &TEST_PIXELS { + image + .set_color_at_3d(*x, *y, z, Color::from(*color)) + .unwrap(); + } + } + + // Grow image + 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!( + image.get_color_at(0, 0), + Ok(Color::LinearRgba(INITIAL_FILL)) + )); + for z in 0..LAYERS { + for (x, y, color) in &TEST_PIXELS { + assert_eq!( + image.get_color_at_3d(*x, *y, z).unwrap(), + Color::LinearRgba(*color) + ); + } + } + + // Pixels in the newly added area should get filled with zeroes. + for z in 0..(LAYERS + 1) { + assert!(matches!( + image.get_color_at_3d(3, 3, z), + Ok(Color::LinearRgba(GROW_FILL)) + )); + } + + // Shrink + 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()); + + // Higher layers should no longer be present + 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, + }); + + // Pixels in the newly added layer should be zeroes. + assert!(matches!( + image.get_color_at_3d(0, 0, 1), + Ok(Color::LinearRgba(GROW_FILL)) + )); + } } diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 0ef1213b46..fe086db674 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -81,19 +81,35 @@ impl ImageLoader { } } +/// How to determine an image's format when loading. #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub enum ImageFormatSetting { + /// Determine the image format from its file extension. + /// + /// This is the default. #[default] FromExtension, + /// Declare the image format explicitly. Format(ImageFormat), + /// Guess the image format by looking for magic bytes at the + /// beginning of its data. Guess, } +/// Settings for loading an [`Image`] using an [`ImageLoader`]. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageLoaderSettings { + /// How to determine the image's format. pub format: ImageFormatSetting, + /// Specifies whether image data is linear + /// or in sRGB space when this is not determined by + /// the image format. pub is_srgb: bool, + /// [`ImageSampler`] to use when rendering - this does + /// not affect the loading of the image data. pub sampler: ImageSampler, + /// Where the asset will be used - see the docs on + /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, } @@ -108,11 +124,14 @@ impl Default for ImageLoaderSettings { } } +/// An error when loading an image using [`ImageLoader`]. #[non_exhaustive] #[derive(Debug, Error)] pub enum ImageLoaderError { - #[error("Could load shader: {0}")] + /// An error occurred while trying to load the image bytes. + #[error("Failed to load image bytes: {0}")] Io(#[from] std::io::Error), + /// An error occurred while trying to decode the image bytes. #[error("Could not load texture file: {0}")] FileTexture(#[from] FileTextureError), } @@ -170,7 +189,7 @@ impl AssetLoader for ImageLoader { /// An error that occurs when loading a texture from a file. #[derive(Error, Debug)] -#[error("Error reading image file {path}: {error}, this is an error in `bevy_render`.")] +#[error("Error reading image file {path}: {error}.")] pub struct FileTextureError { error: TextureError, path: String, diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index 0cccbacb07..61304c2145 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -1,4 +1,4 @@ -#[cfg(any(feature = "flate2", feature = "ruzstd"))] +#[cfg(any(feature = "flate2", feature = "zstd_rust"))] use std::io::Read; #[cfg(feature = "basis-universal")] @@ -7,7 +7,7 @@ use basis_universal::{ }; use bevy_color::Srgba; use bevy_utils::default; -#[cfg(any(feature = "flate2", feature = "ruzstd"))] +#[cfg(any(feature = "flate2", feature = "zstd_rust", feature = "zstd_c"))] use ktx2::SupercompressionScheme; use ktx2::{ ChannelTypeQualifiers, ColorModel, DfdBlockBasic, DfdBlockHeaderBasic, DfdHeader, Header, @@ -58,7 +58,7 @@ pub fn ktx2_buffer_to_image( })?; levels.push(decompressed); } - #[cfg(feature = "ruzstd")] + #[cfg(feature = "zstd_rust")] SupercompressionScheme::Zstandard => { let mut cursor = std::io::Cursor::new(level.data); let mut decoder = ruzstd::decoding::StreamingDecoder::new(&mut cursor) @@ -71,6 +71,14 @@ pub fn ktx2_buffer_to_image( })?; levels.push(decompressed); } + #[cfg(all(feature = "zstd_c", not(feature = "zstd_rust")))] + SupercompressionScheme::Zstandard => { + levels.push(zstd::decode_all(level.data).map_err(|err| { + TextureError::SuperDecompressionError(format!( + "Failed to decompress {supercompression_scheme:?} for mip {level_index}: {err:?}", + )) + })?); + } _ => { return Err(TextureError::SuperDecompressionError(format!( "Unsupported supercompression scheme: {supercompression_scheme:?}", @@ -230,43 +238,20 @@ pub fn ktx2_buffer_to_image( ))); } - // Reorder data from KTX2 MipXLayerYFaceZ to wgpu LayerYFaceZMipX - let texture_format_info = texture_format; - let (block_width_pixels, block_height_pixels) = ( - texture_format_info.block_dimensions().0 as usize, - texture_format_info.block_dimensions().1 as usize, - ); - // Texture is not a depth or stencil format, it is possible to pass `None` and unwrap - let block_bytes = texture_format_info.block_copy_size(None).unwrap() as usize; - - let mut wgpu_data = vec![Vec::default(); (layer_count * face_count) as usize]; - for (level, level_data) in levels.iter().enumerate() { - let (level_width, level_height, level_depth) = ( - (width as usize >> level).max(1), - (height as usize >> level).max(1), - (depth as usize >> level).max(1), - ); - let (num_blocks_x, num_blocks_y) = ( - level_width.div_ceil(block_width_pixels).max(1), - level_height.div_ceil(block_height_pixels).max(1), - ); - let level_bytes = num_blocks_x * num_blocks_y * level_depth * block_bytes; - - let mut index = 0; - for _layer in 0..layer_count { - for _face in 0..face_count { - let offset = index * level_bytes; - wgpu_data[index].extend_from_slice(&level_data[offset..(offset + level_bytes)]); - index += 1; - } - } - } + // Collect all level data into a contiguous buffer + let mut image_data = Vec::new(); + image_data.reserve_exact(levels.iter().map(Vec::len).sum()); + levels.iter().for_each(|level| image_data.extend(level)); // Assign the data and fill in the rest of the metadata now the possible // error cases have been handled let mut image = Image::default(); image.texture_descriptor.format = texture_format; - image.data = Some(wgpu_data.into_iter().flatten().collect::>()); + image.data = Some(image_data); + image.data_order = wgpu_types::TextureDataOrder::MipMajor; + // Note: we must give wgpu the logical texture dimensions, so it can correctly compute mip sizes. + // However this currently causes wgpu to panic if the dimensions arent a multiple of blocksize. + // See https://github.com/gfx-rs/wgpu/issues/7677 for more context. image.texture_descriptor.size = Extent3d { width, height, @@ -276,8 +261,7 @@ pub fn ktx2_buffer_to_image( depth } .max(1), - } - .physical_size(texture_format); + }; image.texture_descriptor.mip_level_count = level_count; image.texture_descriptor.dimension = if depth > 1 { TextureDimension::D3 diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index 55f74a5f14..385afc4933 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -10,11 +10,16 @@ pub mod prelude { }; } +#[cfg(all(feature = "zstd", not(feature = "zstd_rust"), not(feature = "zstd_c")))] +compile_error!( + "Choosing a zstd backend is required for zstd support. Please enable either the \"zstd_rust\" or the \"zstd_c\" feature." +); + 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 +34,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 570273a00a..c32a87a52d 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -42,7 +42,6 @@ std = [ "bevy_app/std", "bevy_ecs/std", "bevy_math/std", - "bevy_utils/std", "bevy_reflect/std", "bevy_platform/std", ] @@ -61,14 +60,13 @@ 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_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 = [ @@ -76,7 +74,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 bc28381ab4..e4ff47f470 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -71,7 +71,7 @@ use { /// Reading and checking against the current set of pressed buttons: /// ```no_run /// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, Update}; -/// # use bevy_ecs::{prelude::{IntoScheduleConfigs, Res, Resource, resource_changed}, schedule::Condition}; +/// # use bevy_ecs::{prelude::{IntoScheduleConfigs, Res, Resource, resource_changed}, schedule::SystemCondition}; /// # use bevy_input::{ButtonInput, prelude::{KeyCode, MouseButton}}; /// /// fn main() { @@ -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..aaef46b3e8 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. @@ -2223,7 +2223,7 @@ mod tests { self.app .world_mut() .resource_mut::>() - .send(GamepadConnectionEvent::new( + .write(GamepadConnectionEvent::new( gamepad, Connected { name: "Test gamepad".to_string(), @@ -2238,14 +2238,14 @@ mod tests { self.app .world_mut() .resource_mut::>() - .send(GamepadConnectionEvent::new(gamepad, Disconnected)); + .write(GamepadConnectionEvent::new(gamepad, Disconnected)); } pub fn send_raw_gamepad_event(&mut self, event: RawGamepadEvent) { self.app .world_mut() .resource_mut::>() - .send(event); + .write(event); } pub fn send_raw_gamepad_event_batch( @@ -2255,7 +2255,7 @@ mod tests { self.app .world_mut() .resource_mut::>() - .send_batch(events); + .write_batch(events); } } @@ -2449,7 +2449,7 @@ mod tests { ctx.app .world_mut() .resource_mut::>() - .send_batch([ + .write_batch([ RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( entity, GamepadAxis::LeftStickY, @@ -2513,7 +2513,7 @@ mod tests { ctx.app .world_mut() .resource_mut::>() - .send_batch(events); + .write_batch(events); ctx.update(); assert_eq!( ctx.app @@ -2550,7 +2550,7 @@ mod tests { ctx.app .world_mut() .resource_mut::>() - .send_batch(events); + .write_batch(events); ctx.update(); assert_eq!( ctx.app @@ -2598,7 +2598,7 @@ mod tests { ctx.app .world_mut() .resource_mut::>() - .send_batch(events); + .write_batch(events); ctx.update(); let events = ctx @@ -2654,7 +2654,7 @@ mod tests { ctx.app .world_mut() .resource_mut::>() - .send_batch(events); + .write_batch(events); ctx.update(); assert_eq!( ctx.app @@ -2692,7 +2692,7 @@ mod tests { ctx.app .world_mut() .resource_mut::>() - .send_batch(events); + .write_batch(events); ctx.update(); let events = ctx @@ -2728,7 +2728,7 @@ mod tests { ctx.app .world_mut() .resource_mut::>() - .send_batch(events); + .write_batch(events); ctx.update(); assert_eq!( 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 67c8995179..77cbe96822 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -1,12 +1,12 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] -//! Input functionality for the [Bevy game engine](https://bevyengine.org/). +//! Input functionality for the [Bevy game engine](https://bevy.org/). //! //! # Supported input devices //! @@ -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, @@ -60,12 +60,13 @@ use touch::{touch_screen_input_system, TouchInput, Touches}; #[cfg(feature = "bevy_reflect")] use gamepad::Gamepad; use gamepad::{ - gamepad_connection_system, gamepad_event_processing_system, GamepadAxis, - GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent, - GamepadButtonStateChangedEvent, GamepadConnection, GamepadConnectionEvent, GamepadEvent, - GamepadInput, GamepadRumbleRequest, GamepadSettings, RawGamepadAxisChangedEvent, - RawGamepadButtonChangedEvent, RawGamepadEvent, + gamepad_connection_system, gamepad_event_processing_system, GamepadAxisChangedEvent, + GamepadButtonChangedEvent, GamepadButtonStateChangedEvent, GamepadConnectionEvent, + GamepadEvent, GamepadRumbleRequest, RawGamepadAxisChangedEvent, RawGamepadButtonChangedEvent, + RawGamepadEvent, }; +#[cfg(feature = "bevy_reflect")] +use gamepad::{GamepadAxis, GamepadButton, GamepadConnection, GamepadInput, GamepadSettings}; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; @@ -89,6 +90,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 0b2ca53830..60b824258d 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_input_focus" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Keyboard focus management" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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/autofocus.rs b/crates/bevy_input_focus/src/autofocus.rs index 72024418d2..f6a862db88 100644 --- a/crates/bevy_input_focus/src/autofocus.rs +++ b/crates/bevy_input_focus/src/autofocus.rs @@ -1,6 +1,6 @@ //! Contains the [`AutoFocus`] component and related machinery. -use bevy_ecs::{component::HookContext, prelude::*, world::DeferredWorld}; +use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld}; use crate::InputFocus; diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 44ff0ef645..b9aa9ffcbb 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -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,30 +392,18 @@ mod tests { use super::*; use alloc::string::String; - use bevy_ecs::{ - component::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()) { @@ -401,14 +413,16 @@ mod tests { } } - 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().write_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().write_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() + .write_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 a5fe691458..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 { @@ -221,7 +222,7 @@ impl TabNavigation<'_, '_> { action: NavAction, ) -> Result { // List of all focusable entities found. - let mut focusable: Vec<(Entity, TabIndex)> = + let mut focusable: Vec<(Entity, TabIndex, usize)> = Vec::with_capacity(self.tabindex_query.iter().len()); match tabgroup { @@ -229,7 +230,7 @@ impl TabNavigation<'_, '_> { // We're in a modal tab group, then gather all tab indices in that group. if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) { for child in children.iter() { - self.gather_focusable(&mut focusable, *child); + self.gather_focusable(&mut focusable, *child, 0); } } } @@ -245,9 +246,12 @@ impl TabNavigation<'_, '_> { tab_groups.sort_by_key(|(_, tg)| tg.order); // Search group descendants - tab_groups.iter().for_each(|(tg_entity, _)| { - self.gather_focusable(&mut focusable, *tg_entity); - }); + tab_groups + .iter() + .enumerate() + .for_each(|(idx, (tg_entity, _))| { + self.gather_focusable(&mut focusable, *tg_entity, idx); + }); } } @@ -255,8 +259,14 @@ impl TabNavigation<'_, '_> { return Err(TabNavigationError::NoFocusableEntities); } - // Stable sort by tabindex - focusable.sort_by_key(|(_, idx)| *idx); + // Sort by TabGroup and then TabIndex + focusable.sort_by(|(_, a_tab_idx, a_group), (_, b_tab_idx, b_group)| { + if a_group == b_group { + a_tab_idx.cmp(b_tab_idx) + } else { + a_group.cmp(b_group) + } + }); let index = focusable.iter().position(|e| Some(e.0) == focus.0); let count = focusable.len(); @@ -267,37 +277,67 @@ impl TabNavigation<'_, '_> { (None, NavAction::Previous) | (_, NavAction::Last) => count - 1, }; match focusable.get(next) { - Some((entity, _)) => Ok(*entity), + Some((entity, _, _)) => Ok(*entity), None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity), } } /// Gather all focusable entities in tree order. - fn gather_focusable(&self, out: &mut Vec<(Entity, TabIndex)>, parent: Entity) { + fn gather_focusable( + &self, + out: &mut Vec<(Entity, TabIndex, usize)>, + parent: Entity, + tab_group_idx: usize, + ) { if let Ok((entity, tabindex, children)) = self.tabindex_query.get(parent) { if let Some(tabindex) = tabindex { if tabindex.0 >= 0 { - out.push((entity, *tabindex)); + out.push((entity, *tabindex, tab_group_idx)); } } if let Some(children) = children { for child in children.iter() { // Don't traverse into tab groups, as they are handled separately. if self.tabgroup_query.get(*child).is_err() { - self.gather_focusable(out, *child); + self.gather_focusable(out, *child, tab_group_idx); } } } } else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent) { if !tabgroup.modal { for child in children.iter() { - self.gather_focusable(out, *child); + self.gather_focusable(out, *child, tab_group_idx); } } } } } +/// 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; @@ -307,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); } } @@ -316,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, @@ -323,7 +389,7 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut trigger: On>, nav: TabNavigation, mut focus: ResMut, mut visible: ResMut, @@ -351,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); @@ -397,4 +463,45 @@ mod tests { let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last); assert_eq!(last_entity, Ok(tab_entity_2)); } + + #[test] + fn test_tab_navigation_between_groups_is_sorted_by_group() { + let mut app = App::new(); + let world = app.world_mut(); + + let tab_group_1 = world.spawn(TabGroup::new(0)).id(); + let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_1))).id(); + let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_1))).id(); + + let tab_group_2 = world.spawn(TabGroup::new(1)).id(); + let tab_entity_3 = world.spawn((TabIndex(0), ChildOf(tab_group_2))).id(); + let tab_entity_4 = world.spawn((TabIndex(1), ChildOf(tab_group_2))).id(); + + let mut system_state: SystemState = SystemState::new(world); + let tab_navigation = system_state.get(world); + assert_eq!(tab_navigation.tabgroup_query.iter().count(), 2); + assert_eq!(tab_navigation.tabindex_query.iter().count(), 4); + + let next_entity = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next); + assert_eq!(next_entity, Ok(tab_entity_2)); + + let prev_entity = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous); + assert_eq!(prev_entity, Ok(tab_entity_1)); + + let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First); + assert_eq!(first_entity, Ok(tab_entity_1)); + + let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last); + assert_eq!(last_entity, Ok(tab_entity_4)); + + let next_from_end_of_group_entity = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Next); + assert_eq!(next_from_end_of_group_entity, Ok(tab_entity_3)); + + let prev_entity_from_start_of_group = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_3), NavAction::Previous); + assert_eq!(prev_entity_from_start_of_group, Ok(tab_entity_2)); + } } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 28d234f2b4..e591803751 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["game", "engine", "gamedev", "graphics", "bevy"] @@ -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"] @@ -37,6 +43,8 @@ ktx2 = ["bevy_image/ktx2", "bevy_render/ktx2"] # For ktx2 supercompression zlib = ["bevy_image/zlib"] zstd = ["bevy_image/zstd"] +zstd_rust = ["bevy_image/zstd_rust"] +zstd_c = ["bevy_image/zstd_c"] # Image format support (PNG enabled by default) bmp = ["bevy_image/bmp"] @@ -100,6 +108,7 @@ serialize = [ "bevy_window?/serialize", "bevy_winit?/serialize", "bevy_platform/serialize", + "bevy_render/serialize", ] multi_threaded = [ "std", @@ -124,6 +133,15 @@ pbr_transmission_textures = [ "bevy_gltf?/pbr_transmission_textures", ] +# Clustered Decal support +pbr_clustered_decals = ["bevy_pbr?/pbr_clustered_decals"] + +# Light Texture support +pbr_light_textures = [ + "bevy_pbr?/pbr_clustered_decals", + "bevy_pbr?/pbr_light_textures", +] + # Multi-layer material textures in `StandardMaterial`: pbr_multi_layer_material_textures = [ "bevy_pbr?/pbr_multi_layer_material_textures", @@ -178,8 +196,13 @@ bevy_anti_aliasing = ["dep:bevy_anti_aliasing", "bevy_image"] bevy_gizmos = ["dep:bevy_gizmos", "bevy_image"] bevy_gltf = ["dep:bevy_gltf", "bevy_image"] bevy_ui = ["dep:bevy_ui", "bevy_image"] +bevy_ui_render = ["dep:bevy_ui_render"] bevy_image = ["dep:bevy_image"] +bevy_mesh = ["dep:bevy_mesh", "bevy_image"] +bevy_camera = ["dep:bevy_camera", "bevy_mesh"] +bevy_light = ["dep:bevy_light", "bevy_camera"] + # Used to disable code that is unsupported when Bevy is dynamically linked dynamic_linking = ["bevy_diagnostic/dynamic_linking"] @@ -196,7 +219,7 @@ bevy_render = [ "dep:bevy_render", "bevy_scene?/bevy_render", "bevy_gizmos?/bevy_render", - "bevy_image", + "bevy_camera", "bevy_color/wgpu-types", "bevy_color/encase", ] @@ -255,7 +278,7 @@ bevy_sprite_picking_backend = [ bevy_ui_picking_backend = ["bevy_picking", "bevy_ui/bevy_ui_picking_backend"] # Provides a UI debug overlay -bevy_ui_debug = ["bevy_ui?/bevy_ui_debug"] +bevy_ui_debug = ["bevy_ui_render?/bevy_ui_debug"] # Enable built in global state machines bevy_state = ["dep:bevy_state"] @@ -279,9 +302,6 @@ custom_cursor = ["bevy_winit/custom_cursor"] # Experimental support for nodes that are ignored for UI layouting ghost_nodes = ["bevy_ui/ghost_nodes"] -# Use the configurable global error handler as the default error handler. -configurable_error_handler = ["bevy_ecs/configurable_error_handler"] - # Allows access to the `std` crate. Enabling this feature will prevent compilation # on `no_std` targets, but provides access to certain additional features on # supported platforms. @@ -299,7 +319,6 @@ std = [ "bevy_state?/std", "bevy_time/std", "bevy_transform/std", - "bevy_utils/std", "bevy_tasks/std", "bevy_window?/std", ] @@ -317,7 +336,6 @@ critical-section = [ "bevy_reflect/critical-section", "bevy_state?/critical-section", "bevy_time/critical-section", - "bevy_utils/critical-section", "bevy_tasks/critical-section", ] @@ -349,82 +367,95 @@ web = [ "bevy_tasks/web", ] +hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] + +gltf_convert_coordinates_default = [ + "bevy_gltf?/gltf_convert_coordinates_default", +] + +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, features = [ - "alloc", -] } -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_feathers = { path = "../bevy_feathers", optional = true, version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", optional = true, version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", optional = true, version = "0.17.0-dev" } +bevy_light = { path = "../bevy_light", 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_ui_render = { path = "../bevy_ui_render", 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/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index db1152a362..cdb59921dc 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -19,7 +19,7 @@ plugin_group! { #[cfg(feature = "bevy_window")] bevy_a11y:::AccessibilityPlugin, #[cfg(feature = "std")] - #[custom(cfg(any(unix, windows)))] + #[custom(cfg(any(all(unix, not(target_os = "horizon")), windows)))] bevy_app:::TerminalCtrlCHandlerPlugin, #[cfg(feature = "bevy_asset")] bevy_asset:::AssetPlugin, @@ -46,6 +46,8 @@ plugin_group! { bevy_text:::TextPlugin, #[cfg(feature = "bevy_ui")] bevy_ui:::UiPlugin, + #[cfg(feature = "bevy_ui_render")] + bevy_ui_render:::UiRenderPlugin, #[cfg(feature = "bevy_pbr")] bevy_pbr:::PbrPlugin, // NOTE: Load this after renderer initialization so that it knows about the supported @@ -66,6 +68,8 @@ plugin_group! { bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] bevy_dev_tools::ci_testing:::CiTestingPlugin, + #[cfg(feature = "hotpatching")] + bevy_app::hotpatch:::HotPatchPlugin, #[plugin_group] #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 07dd936ab1..4f965e603a 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -25,14 +25,20 @@ pub use bevy_app as app; pub use bevy_asset as asset; #[cfg(feature = "bevy_audio")] pub use bevy_audio as audio; +#[cfg(feature = "bevy_camera")] +pub use bevy_camera as camera; #[cfg(feature = "bevy_color")] 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; pub use bevy_ecs as ecs; +#[cfg(feature = "bevy_feathers")] +pub use bevy_feathers as feathers; #[cfg(feature = "bevy_gilrs")] pub use bevy_gilrs as gilrs; #[cfg(feature = "bevy_gizmos")] @@ -44,9 +50,13 @@ pub use bevy_image as image; pub use bevy_input as input; #[cfg(feature = "bevy_input_focus")] pub use bevy_input_focus as input_focus; +#[cfg(feature = "bevy_light")] +pub use bevy_light as light; #[cfg(feature = "bevy_log")] pub use bevy_log as log; pub use bevy_math as math; +#[cfg(feature = "bevy_mesh")] +pub use bevy_mesh as mesh; #[cfg(feature = "bevy_pbr")] pub use bevy_pbr as pbr; #[cfg(feature = "bevy_picking")] @@ -60,6 +70,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")] @@ -71,6 +83,8 @@ pub use bevy_time as time; pub use bevy_transform as transform; #[cfg(feature = "bevy_ui")] pub use bevy_ui as ui; +#[cfg(feature = "bevy_ui_render")] +pub use bevy_ui_render as ui_render; pub use bevy_utils as utils; #[cfg(feature = "bevy_window")] pub use bevy_window as window; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 26d5c7e2af..c8ba27ea82 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -63,6 +63,10 @@ pub use crate::text::prelude::*; #[cfg(feature = "bevy_ui")] pub use crate::ui::prelude::*; +#[doc(hidden)] +#[cfg(feature = "bevy_ui_render")] +pub use crate::ui_render::prelude::*; + #[doc(hidden)] #[cfg(feature = "bevy_gizmos")] pub use crate::gizmos::prelude::*; diff --git a/crates/bevy_light/Cargo.toml b/crates/bevy_light/Cargo.toml new file mode 100644 index 0000000000..6a3807f9bb --- /dev/null +++ b/crates/bevy_light/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "bevy_light" +version = "0.17.0-dev" +edition = "2024" +description = "Keeps the lights on at Bevy Engine" +homepage = "https://bevy.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_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", 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_camera = { path = "../bevy_camera", 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_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ + "serialize", +] } + +# other +tracing = { version = "0.1", default-features = false } + +[features] +default = [] +experimental_pbr_pcss = [] +webgl = [] +webgpu = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_light/LICENSE-APACHE b/crates/bevy_light/LICENSE-APACHE new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/crates/bevy_light/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_light/LICENSE-MIT b/crates/bevy_light/LICENSE-MIT new file mode 100644 index 0000000000..9cf106272a --- /dev/null +++ b/crates/bevy_light/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_pbr/src/light/ambient_light.rs b/crates/bevy_light/src/ambient_light.rs similarity index 81% rename from crates/bevy_pbr/src/light/ambient_light.rs rename to crates/bevy_light/src/ambient_light.rs index db255722b3..92935e7e06 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_light/src/ambient_light.rs @@ -1,8 +1,11 @@ -use super::*; +use bevy_camera::Camera; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; /// An ambient light, which lights the entire scene equally. /// -/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light. +/// This resource is inserted by the [`LightPlugin`] and by default it is set to a low ambient light. /// /// It can also be added to a camera to override the resource (or default) ambient for that camera only. /// @@ -12,12 +15,14 @@ use super::*; /// /// ``` /// # use bevy_ecs::system::ResMut; -/// # use bevy_pbr::AmbientLight; +/// # use bevy_light::AmbientLight; /// fn setup_ambient_light(mut ambient_light: ResMut) { /// ambient_light.brightness = 100.0; /// } /// ``` -#[derive(Resource, Component, Clone, Debug, ExtractResource, ExtractComponent, Reflect)] +/// +/// [`LightPlugin`]: crate::LightPlugin +#[derive(Resource, Component, Clone, Debug, Reflect)] #[reflect(Resource, Component, Debug, Default, Clone)] #[require(Camera)] pub struct AmbientLight { @@ -48,6 +53,7 @@ impl Default for AmbientLight { } } } + impl AmbientLight { pub const NONE: AmbientLight = AmbientLight { color: Color::WHITE, diff --git a/crates/bevy_light/src/cascade.rs b/crates/bevy_light/src/cascade.rs new file mode 100644 index 0000000000..0cb713a9e6 --- /dev/null +++ b/crates/bevy_light/src/cascade.rs @@ -0,0 +1,333 @@ +pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES}; +use bevy_camera::{Camera, Projection}; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_math::{ops, Mat4, Vec3A, Vec4}; +use bevy_reflect::prelude::*; +use bevy_transform::components::GlobalTransform; + +use crate::{DirectionalLight, DirectionalLightShadowMap}; + +/// Controls how cascaded shadow mapping works. +/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance. +/// +/// ``` +/// # use bevy_light::CascadeShadowConfig; +/// # use bevy_light::CascadeShadowConfigBuilder; +/// # use bevy_utils::default; +/// # +/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder { +/// maximum_distance: 100.0, +/// ..default() +/// }.into(); +/// ``` +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct CascadeShadowConfig { + /// The (positive) distance to the far boundary of each cascade. + pub bounds: Vec, + /// The proportion of overlap each cascade has with the previous cascade. + pub overlap_proportion: f32, + /// The (positive) distance to the near boundary of the first cascade. + pub minimum_distance: f32, +} + +impl Default for CascadeShadowConfig { + fn default() -> Self { + CascadeShadowConfigBuilder::default().into() + } +} + +fn calculate_cascade_bounds( + num_cascades: usize, + nearest_bound: f32, + shadow_maximum_distance: f32, +) -> Vec { + if num_cascades == 1 { + return vec![shadow_maximum_distance]; + } + let base = ops::powf( + shadow_maximum_distance / nearest_bound, + 1.0 / (num_cascades - 1) as f32, + ); + (0..num_cascades) + .map(|i| nearest_bound * ops::powf(base, i as f32)) + .collect() +} + +/// Builder for [`CascadeShadowConfig`]. +pub struct CascadeShadowConfigBuilder { + /// The number of shadow cascades. + /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas + /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing + /// blocky looking shadows. + /// + /// This does come at the cost increased rendering overhead, however this overhead is still less + /// than if you were to use fewer cascades and much larger shadow map textures to achieve the + /// same quality level. + /// + /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may + /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing + /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately. + pub num_cascades: usize, + /// The minimum shadow distance, which can help improve the texel resolution of the first cascade. + /// Areas nearer to the camera than this will likely receive no shadows. + /// + /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as + /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the + /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane + /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to + /// `first_cascade_far_bound`. + pub minimum_distance: f32, + /// The maximum shadow distance. + /// Areas further from the camera than this will likely receive no shadows. + pub maximum_distance: f32, + /// Sets the far bound of the first cascade, relative to the view origin. + /// In-between cascades will be exponentially spaced relative to the maximum shadow distance. + /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence. + pub first_cascade_far_bound: f32, + /// Sets the overlap proportion between cascades. + /// The overlap is used to make the transition from one cascade's shadow map to the next + /// less abrupt by blending between both shadow maps. + pub overlap_proportion: f32, +} + +impl CascadeShadowConfigBuilder { + /// Returns the cascade config as specified by this builder. + pub fn build(&self) -> CascadeShadowConfig { + assert!( + self.num_cascades > 0, + "num_cascades must be positive, but was {}", + self.num_cascades + ); + assert!( + self.minimum_distance >= 0.0, + "maximum_distance must be non-negative, but was {}", + self.minimum_distance + ); + assert!( + self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound, + "minimum_distance must be less than first_cascade_far_bound, but was {}", + self.minimum_distance + ); + assert!( + self.maximum_distance > self.minimum_distance, + "maximum_distance must be greater than minimum_distance, but was {}", + self.maximum_distance + ); + assert!( + (0.0..1.0).contains(&self.overlap_proportion), + "overlap_proportion must be in [0.0, 1.0) but was {}", + self.overlap_proportion + ); + CascadeShadowConfig { + bounds: calculate_cascade_bounds( + self.num_cascades, + self.first_cascade_far_bound, + self.maximum_distance, + ), + overlap_proportion: self.overlap_proportion, + minimum_distance: self.minimum_distance, + } + } +} + +impl Default for CascadeShadowConfigBuilder { + fn default() -> Self { + // The defaults are chosen to be similar to be Unity, Unreal, and Godot. + // Unity: first cascade far bound = 10.05, maximum distance = 150.0 + // Unreal Engine 5: maximum distance = 200.0 + // Godot: first cascade far bound = 10.0, maximum distance = 100.0 + Self { + // Currently only support one cascade in WebGL 2. + num_cascades: if cfg!(all( + feature = "webgl", + target_arch = "wasm32", + not(feature = "webgpu") + )) { + 1 + } else { + 4 + }, + minimum_distance: 0.1, + maximum_distance: 150.0, + first_cascade_far_bound: 10.0, + overlap_proportion: 0.2, + } + } +} + +impl From for CascadeShadowConfig { + fn from(builder: CascadeShadowConfigBuilder) -> Self { + builder.build() + } +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct Cascades { + /// Map from a view to the configuration of each of its [`Cascade`]s. + pub cascades: EntityHashMap>, +} + +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Clone, Default)] +pub struct Cascade { + /// The transform of the light, i.e. the view to world matrix. + pub world_from_cascade: Mat4, + /// The orthographic projection for this cascade. + pub clip_from_cascade: Mat4, + /// The view-projection matrix for this cascade, converting world space into light clip space. + /// Importantly, this is derived and stored separately from `view_transform` and `projection` to + /// ensure shadow stability. + pub clip_from_world: Mat4, + /// Size of each shadow map texel in world units. + pub texel_size: f32, +} + +pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { + for (directional_light, mut cascades) in lights.iter_mut() { + if !directional_light.shadows_enabled { + continue; + } + cascades.cascades.clear(); + } +} + +pub fn build_directional_light_cascades( + directional_light_shadow_map: Res, + views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, + mut lights: Query<( + &GlobalTransform, + &DirectionalLight, + &CascadeShadowConfig, + &mut Cascades, + )>, +) { + let views = views + .iter() + .filter_map(|(entity, transform, projection, camera)| { + if camera.is_active { + Some((entity, projection, transform.to_matrix())) + } else { + None + } + }) + .collect::>(); + + for (transform, directional_light, cascades_config, mut cascades) in &mut lights { + if !directional_light.shadows_enabled { + continue; + } + + // It is very important to the numerical and thus visual stability of shadows that + // 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.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(); + + for (view_entity, projection, view_to_world) in views.iter().copied() { + let camera_to_light_view = light_to_world_inverse * view_to_world; + let view_cascades = cascades_config + .bounds + .iter() + .enumerate() + .map(|(idx, far_bound)| { + // Negate bounds as -z is camera forward direction. + let z_near = if idx > 0 { + (1.0 - cascades_config.overlap_proportion) + * -cascades_config.bounds[idx - 1] + } else { + -cascades_config.minimum_distance + }; + let z_far = -far_bound; + + let corners = projection.get_frustum_corners(z_near, z_far); + + calculate_cascade( + corners, + directional_light_shadow_map.size as f32, + world_from_light, + camera_to_light_view, + ) + }) + .collect(); + cascades.cascades.insert(view_entity, view_cascades); + } + } +} + +/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. +/// +/// The corner vertices should be specified in the following order: +/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane. +fn calculate_cascade( + frustum_corners: [Vec3A; 8], + cascade_texture_size: f32, + world_from_light: Mat4, + light_from_camera: Mat4, +) -> Cascade { + let mut min = Vec3A::splat(f32::MAX); + let mut max = Vec3A::splat(f32::MIN); + for corner_camera_view in frustum_corners { + let corner_light_view = light_from_camera.transform_point3a(corner_camera_view); + min = min.min(corner_light_view); + max = max.max(corner_light_view); + } + + // NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this + // will be the maximum possible projection size. Use the ceiling to get an integer which is + // very important for floating point stability later. It is also important that these are + // calculated using the original camera space corner positions for floating point precision + // as even though the lengths using corner_light_view above should be the same, precision can + // introduce small but significant differences. + // NOTE: The size remains the same unless the view frustum or cascade configuration is modified. + let cascade_diameter = (frustum_corners[0] - frustum_corners[6]) + .length() + .max((frustum_corners[4] - frustum_corners[6]).length()) + .ceil(); + + // NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an + // integer, cascade_texel_size is then an integer multiple of a power of 2 and can be + // exactly represented in a floating point value. + let cascade_texel_size = cascade_diameter / cascade_texture_size; + // NOTE: For shadow stability it is very important that the near_plane_center is at integer + // multiples of the texel size to be exactly representable in a floating point value. + let near_plane_center = Vec3A::new( + (0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size, + (0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size, + // NOTE: max.z is the near plane for right-handed y-up + max.z, + ); + + // It is critical for `world_to_cascade` to be stable. So rather than forming `cascade_to_world` + // and inverting it, which risks instability due to numerical precision, we directly form + // `world_to_cascade` as the reference material suggests. + let light_to_world_transpose = world_from_light.transpose(); + let cascade_from_world = Mat4::from_cols( + light_to_world_transpose.x_axis, + light_to_world_transpose.y_axis, + light_to_world_transpose.z_axis, + (-near_plane_center).extend(1.0), + ); + + // Right-handed orthographic projection, centered at `near_plane_center`. + // NOTE: This is different from the reference material, as we use reverse Z. + let r = (max.z - min.z).recip(); + let clip_from_cascade = Mat4::from_cols( + Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0), + Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0), + Vec4::new(0.0, 0.0, r, 0.0), + Vec4::new(0.0, 0.0, 1.0, 1.0), + ); + + let clip_from_world = clip_from_cascade * cascade_from_world; + Cascade { + world_from_cascade: cascade_from_world.inverse(), + clip_from_cascade, + clip_from_world, + texel_size: cascade_texel_size, + } +} diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_light/src/cluster/assign.rs similarity index 94% rename from crates/bevy_pbr/src/cluster/assign.rs rename to crates/bevy_light/src/cluster/assign.rs index 1b7b3563d7..20a40104ed 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_light/src/cluster/assign.rs @@ -1,5 +1,10 @@ //! Assigning objects to clusters. +use bevy_camera::{ + primitives::{Aabb, Frustum, HalfSpace, Sphere}, + visibility::{RenderLayers, ViewVisibility}, + Camera, +}; use bevy_ecs::{ entity::Entity, query::{Has, With}, @@ -9,25 +14,15 @@ use bevy_math::{ ops::{self, sin_cos}, Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _, }; -use bevy_render::{ - camera::Camera, - primitives::{Aabb, Frustum, HalfSpace, Sphere}, - render_resource::BufferBindingType, - renderer::{RenderAdapter, RenderDevice}, - view::{RenderLayers, ViewVisibility}, -}; use bevy_transform::components::GlobalTransform; use bevy_utils::prelude::default; use tracing::warn; -use crate::{ - decal::{self, clustered::ClusteredDecal}, - prelude::EnvironmentMapLight, - ClusterConfig, ClusterFarZMode, Clusters, ExtractedPointLight, GlobalVisibleClusterableObjects, - LightProbe, PointLight, SpotLight, ViewClusterBindings, VisibleClusterableObjects, - VolumetricLight, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, - MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, +use super::{ + ClusterConfig, ClusterFarZMode, ClusteredDecal, Clusters, GlobalClusterSettings, + GlobalVisibleClusterableObjects, VisibleClusterableObjects, }; +use crate::{EnvironmentMapLight, LightProbe, PointLight, SpotLight, VolumetricLight}; const NDC_MIN: Vec2 = Vec2::NEG_ONE; const NDC_MAX: Vec2 = Vec2::ONE; @@ -59,7 +54,7 @@ impl ClusterableObjectAssignmentData { /// Data needed to assign objects to clusters that's specific to the type of /// clusterable object. #[derive(Clone, Copy, Debug)] -pub(crate) enum ClusterableObjectType { +pub enum ClusterableObjectType { /// Data needed to assign point lights to clusters. PointLight { /// Whether shadows are enabled for this point light. @@ -107,7 +102,7 @@ impl ClusterableObjectType { /// Generally, we sort first by type, then, for lights, by whether shadows /// are enabled (enabled before disabled), and then whether volumetrics are /// enabled (enabled before disabled). - pub(crate) fn ordering(&self) -> (u8, bool, bool) { + pub fn ordering(&self) -> (u8, bool, bool) { match *self { ClusterableObjectType::PointLight { shadows_enabled, @@ -123,23 +118,6 @@ impl ClusterableObjectType { ClusterableObjectType::Decal => (4, false, false), } } - - /// Creates the [`ClusterableObjectType`] data for a point or spot light. - pub(crate) fn from_point_or_spot_light( - point_light: &ExtractedPointLight, - ) -> ClusterableObjectType { - match point_light.spot_light_angles { - Some((_, outer_angle)) => ClusterableObjectType::SpotLight { - outer_angle, - shadows_enabled: point_light.shadows_enabled, - volumetric: point_light.volumetric, - }, - None => ClusterableObjectType::PointLight { - shadows_enabled: point_light.shadows_enabled, - volumetric: point_light.volumetric, - }, - } - } } // NOTE: Run this before update_point_light_frusta! @@ -180,9 +158,9 @@ pub(crate) fn assign_objects_to_clusters( mut clusterable_objects: Local>, mut cluster_aabb_spheres: Local>>, mut max_clusterable_objects_warning_emitted: Local, - (render_device, render_adapter): (Option>, Option>), + global_cluster_settings: Option>, ) { - let (Some(render_device), Some(render_adapter)) = (render_device, render_adapter) else { + let Some(global_cluster_settings) = global_cluster_settings else { return; }; @@ -229,20 +207,13 @@ pub(crate) fn assign_objects_to_clusters( ), ); - let clustered_forward_buffer_binding_type = - render_device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); - let supports_storage_buffers = matches!( - clustered_forward_buffer_binding_type, - BufferBindingType::Storage { .. } - ); - // Gather up light probes, but only if we're clustering them. // // UBOs aren't large enough to hold indices for light probes, so we can't // cluster light probes on such platforms (mainly WebGL 2). Besides, those // platforms typically lack bindless textures, so multiple light probes // wouldn't be supported anyhow. - if supports_storage_buffers { + if global_cluster_settings.supports_storage_buffers { clusterable_objects.extend(light_probes_query.iter().map( |(entity, transform, is_reflection_probe)| ClusterableObjectAssignmentData { entity, @@ -259,7 +230,7 @@ pub(crate) fn assign_objects_to_clusters( } // Add decals if the current platform supports them. - if decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) { + if global_cluster_settings.clustered_decals_are_usable { clusterable_objects.extend(decals_query.iter().map(|(entity, transform)| { ClusterableObjectAssignmentData { entity, @@ -271,8 +242,8 @@ pub(crate) fn assign_objects_to_clusters( })); } - if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS - && !supports_storage_buffers + if clusterable_objects.len() > global_cluster_settings.max_uniform_buffer_clusterable_objects + && !global_cluster_settings.supports_storage_buffers { clusterable_objects.sort_by_cached_key(|clusterable_object| { ( @@ -290,7 +261,9 @@ pub(crate) fn assign_objects_to_clusters( let mut clusterable_objects_in_view_count = 0; clusterable_objects.retain(|clusterable_object| { // take one extra clusterable object to check if we should emit the warning - if clusterable_objects_in_view_count == MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS + 1 { + if clusterable_objects_in_view_count + == global_cluster_settings.max_uniform_buffer_clusterable_objects + 1 + { false } else { let clusterable_object_sphere = clusterable_object.sphere(); @@ -306,17 +279,19 @@ pub(crate) fn assign_objects_to_clusters( } }); - if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS + if clusterable_objects.len() + > global_cluster_settings.max_uniform_buffer_clusterable_objects && !*max_clusterable_objects_warning_emitted { warn!( - "MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS ({}) exceeded", - MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS + "max_uniform_buffer_clusterable_objects ({}) exceeded", + global_cluster_settings.max_uniform_buffer_clusterable_objects ); *max_clusterable_objects_warning_emitted = true; } - clusterable_objects.truncate(MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS); + clusterable_objects + .truncate(global_cluster_settings.max_uniform_buffer_clusterable_objects); } for ( @@ -353,7 +328,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(); @@ -392,7 +367,7 @@ pub(crate) fn assign_objects_to_clusters( // NOTE: Ensure the far_z is at least as far as the first_depth_slice to avoid clustering problems. let far_z = far_z.max(first_slice_depth); - let cluster_factors = crate::calculate_cluster_factors( + let cluster_factors = calculate_cluster_factors( first_slice_depth, far_z, requested_cluster_dimensions.z as f32, @@ -456,14 +431,17 @@ pub(crate) fn assign_objects_to_clusters( (xy_count.x + x_overlap) * (xy_count.y + y_overlap) * z_count as f32; } - if cluster_index_estimate > ViewClusterBindings::MAX_INDICES as f32 { + if cluster_index_estimate + > global_cluster_settings.view_cluster_bindings_max_indices as f32 + { // scale x and y cluster count to be able to fit all our indices // we take the ratio of the actual indices over the index estimate. // this is not guaranteed to be small enough due to overlapped tiles, but // the conservative estimate is more than sufficient to cover the // difference - let index_ratio = ViewClusterBindings::MAX_INDICES as f32 / cluster_index_estimate; + let index_ratio = global_cluster_settings.view_cluster_bindings_max_indices as f32 + / cluster_index_estimate; let xy_ratio = index_ratio.sqrt(); requested_cluster_dimensions.x = @@ -882,6 +860,23 @@ pub(crate) fn assign_objects_to_clusters( } } +pub fn calculate_cluster_factors( + near: f32, + far: f32, + z_slices: f32, + is_orthographic: bool, +) -> Vec2 { + if is_orthographic { + Vec2::new(-near, z_slices / (-far - -near)) + } else { + let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / ops::ln(far / near); + Vec2::new( + z_slices_of_ln_zfar_over_znear, + ops::ln(near) * z_slices_of_ln_zfar_over_znear, + ) + } +} + fn compute_aabb_for_cluster( z_near: f32, z_far: f32, diff --git a/crates/bevy_light/src/cluster/mod.rs b/crates/bevy_light/src/cluster/mod.rs new file mode 100644 index 0000000000..92f1c5723e --- /dev/null +++ b/crates/bevy_light/src/cluster/mod.rs @@ -0,0 +1,343 @@ +//! Spatial clustering of objects, currently just point and spot lights. + +use bevy_asset::Handle; +use bevy_camera::{ + visibility::{self, Visibility, VisibilityClass}, + Camera, Camera3d, +}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{With, Without}, + reflect::ReflectComponent, + resource::Resource, + system::{Commands, Query}, +}; +use bevy_image::Image; +use bevy_math::{AspectRatio, UVec2, UVec3, Vec3Swizzles as _}; +use bevy_platform::collections::HashSet; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_transform::components::Transform; +use tracing::warn; + +pub mod assign; + +#[cfg(test)] +mod test; + +// Clustered-forward rendering notes +// The main initial reference material used was this rather accessible article: +// http://www.aortiz.me/2018/12/21/CG.html +// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of: +// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/ +// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.) +// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016: +// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf + +#[derive(Resource)] +pub struct GlobalClusterSettings { + pub supports_storage_buffers: bool, + pub clustered_decals_are_usable: bool, + pub max_uniform_buffer_clusterable_objects: usize, + pub view_cluster_bindings_max_indices: usize, +} + +/// Configure the far z-plane mode used for the furthest depth slice for clustered forward +/// rendering +#[derive(Debug, Copy, Clone, Reflect)] +#[reflect(Clone)] +pub enum ClusterFarZMode { + /// Calculate the required maximum z-depth based on currently visible + /// clusterable objects. Makes better use of available clusters, speeding + /// up GPU lighting operations at the expense of some CPU time and using + /// more indices in the clusterable object index lists. + MaxClusterableObjectRange, + /// Constant max z-depth + Constant(f32), +} + +/// Configure the depth-slicing strategy for clustered forward rendering +#[derive(Debug, Copy, Clone, Reflect)] +#[reflect(Default, Clone)] +pub struct ClusterZConfig { + /// Far `Z` plane of the first depth slice + pub first_slice_depth: f32, + /// Strategy for how to evaluate the far `Z` plane of the furthest depth slice + pub far_z_mode: ClusterFarZMode, +} + +/// Configuration of the clustering strategy for clustered forward rendering +#[derive(Debug, Copy, Clone, Component, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub enum ClusterConfig { + /// Disable cluster calculations for this view + None, + /// One single cluster. Optimal for low-light complexity scenes or scenes where + /// most lights affect the entire scene. + Single, + /// Explicit `X`, `Y` and `Z` counts (may yield non-square `X/Y` clusters depending on the aspect ratio) + XYZ { + dimensions: UVec3, + z_config: ClusterZConfig, + /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding + /// the available cluster-object index limit + dynamic_resizing: bool, + }, + /// Fixed number of `Z` slices, `X` and `Y` calculated to give square clusters + /// with at most total clusters. For top-down games where lights will generally always be within a + /// short depth range, it may be useful to use this configuration with 1 or few `Z` slices. This + /// would reduce the number of lights per cluster by distributing more clusters in screen space + /// `X/Y` which matches how lights are distributed in the scene. + FixedZ { + total: u32, + z_slices: u32, + z_config: ClusterZConfig, + /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding + /// the available clusterable object index limit + dynamic_resizing: bool, + }, +} + +#[derive(Component, Debug, Default)] +pub struct Clusters { + /// Tile size + pub tile_size: UVec2, + /// Number of clusters in `X` / `Y` / `Z` in the view frustum + pub dimensions: UVec3, + /// Distance to the far plane of the first depth slice. The first depth slice is special + /// and explicitly-configured to avoid having unnecessarily many slices close to the camera. + pub near: f32, + pub far: f32, + pub clusterable_objects: Vec, +} + +/// The [`VisibilityClass`] used for clusterables (decals, point lights, directional lights, and spot lights). +/// +/// [`VisibilityClass`]: bevy_camera::visibility::VisibilityClass +pub struct ClusterVisibilityClass; + +#[derive(Clone, Component, Debug, Default)] +pub struct VisibleClusterableObjects { + pub entities: Vec, + pub counts: ClusterableObjectCounts, +} + +#[derive(Resource, Default)] +pub struct GlobalVisibleClusterableObjects { + pub(crate) entities: HashSet, +} + +/// Stores the number of each type of clusterable object in a single cluster. +/// +/// Note that `reflection_probes` and `irradiance_volumes` won't be clustered if +/// fewer than 3 SSBOs are available, which usually means on WebGL 2. +#[derive(Clone, Copy, Default, Debug)] +pub struct ClusterableObjectCounts { + /// The number of point lights in the cluster. + pub point_lights: u32, + /// The number of spot lights in the cluster. + pub spot_lights: u32, + /// The number of reflection probes in the cluster. + pub reflection_probes: u32, + /// The number of irradiance volumes in the cluster. + pub irradiance_volumes: u32, + /// The number of decals in the cluster. + pub decals: u32, +} + +/// 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 +/// you may find [`Transform::looking_at`] useful). +/// +/// Clustered decals are the highest-quality types of decals that Bevy supports, +/// but they require bindless textures. This means that they presently can't be +/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used +/// with forward or deferred rendering and don't require a prepass. +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component, Debug, Clone)] +#[require(Transform, Visibility, VisibilityClass)] +#[component(on_add = visibility::add_visibility_class::)] +pub struct ClusteredDecal { + /// The image that the clustered decal projects. + /// + /// This must be a 2D image. If it has an alpha channel, it'll be alpha + /// blended with the underlying surface and/or other decals. All decal + /// images in the scene must use the same sampler. + pub image: Handle, + + /// An application-specific tag you can use for any purpose you want. + /// + /// See the `clustered_decals` example for an example of use. + pub tag: u32, +} + +impl Default for ClusterZConfig { + fn default() -> Self { + Self { + first_slice_depth: 5.0, + far_z_mode: ClusterFarZMode::MaxClusterableObjectRange, + } + } +} + +impl Default for ClusterConfig { + fn default() -> Self { + // 24 depth slices, square clusters with at most 4096 total clusters + // use max light distance as clusters max `Z`-depth, first slice extends to 5.0 + Self::FixedZ { + total: 4096, + z_slices: 24, + z_config: ClusterZConfig::default(), + dynamic_resizing: true, + } + } +} + +impl ClusterConfig { + fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 { + match &self { + ClusterConfig::None => UVec3::ZERO, + ClusterConfig::Single => UVec3::ONE, + ClusterConfig::XYZ { dimensions, .. } => *dimensions, + ClusterConfig::FixedZ { + total, z_slices, .. + } => { + let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y) + .expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values") + .ratio(); + let mut z_slices = *z_slices; + if *total < z_slices { + warn!("ClusterConfig has more z-slices than total clusters!"); + z_slices = *total; + } + let per_layer = *total as f32 / z_slices as f32; + + let y = f32::sqrt(per_layer / aspect_ratio); + + let mut x = (y * aspect_ratio) as u32; + let mut y = y as u32; + + // check extremes + if x == 0 { + x = 1; + y = per_layer as u32; + } + if y == 0 { + x = per_layer as u32; + y = 1; + } + + UVec3::new(x, y, z_slices) + } + } + } + + fn first_slice_depth(&self) -> f32 { + match self { + ClusterConfig::None | ClusterConfig::Single => 0.0, + ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => { + z_config.first_slice_depth + } + } + } + + fn far_z_mode(&self) -> ClusterFarZMode { + match self { + ClusterConfig::None => ClusterFarZMode::Constant(0.0), + ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange, + ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => { + z_config.far_z_mode + } + } + } + + fn dynamic_resizing(&self) -> bool { + match self { + ClusterConfig::None | ClusterConfig::Single => false, + ClusterConfig::XYZ { + dynamic_resizing, .. + } + | ClusterConfig::FixedZ { + dynamic_resizing, .. + } => *dynamic_resizing, + } + } +} + +impl Clusters { + fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) { + debug_assert!( + requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0 + ); + + let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2()) + .ceil() + .as_uvec2() + .max(UVec2::ONE); + self.tile_size = tile_size; + self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2()) + .ceil() + .as_uvec2() + .extend(requested_dimensions.z) + .max(UVec3::ONE); + + // NOTE: Maximum 4096 clusters due to uniform buffer size constraints + debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096); + } + fn clear(&mut self) { + self.tile_size = UVec2::ONE; + self.dimensions = UVec3::ZERO; + self.near = 0.0; + self.far = 0.0; + self.clusterable_objects.clear(); + } +} + +pub fn add_clusters( + mut commands: Commands, + cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), (Without, With)>, +) { + for (entity, config, camera) in &cameras { + if !camera.is_active { + continue; + } + + let config = config.copied().unwrap_or_default(); + // actual settings here don't matter - they will be overwritten in + // `assign_objects_to_clusters`` + commands + .entity(entity) + .insert((Clusters::default(), config)); + } +} + +impl VisibleClusterableObjects { + #[inline] + pub fn iter(&self) -> impl DoubleEndedIterator { + self.entities.iter() + } + + #[inline] + pub fn len(&self) -> usize { + self.entities.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.entities.is_empty() + } +} + +impl GlobalVisibleClusterableObjects { + #[inline] + pub fn iter(&self) -> impl Iterator { + self.entities.iter() + } + + #[inline] + pub fn contains(&self, entity: Entity) -> bool { + self.entities.contains(&entity) + } +} diff --git a/crates/bevy_pbr/src/cluster/test.rs b/crates/bevy_light/src/cluster/test.rs similarity index 98% rename from crates/bevy_pbr/src/cluster/test.rs rename to crates/bevy_light/src/cluster/test.rs index 23809da7f6..4939d9cb74 100644 --- a/crates/bevy_pbr/src/cluster/test.rs +++ b/crates/bevy_light/src/cluster/test.rs @@ -1,6 +1,6 @@ use bevy_math::UVec2; -use crate::{ClusterConfig, Clusters}; +use super::{ClusterConfig, Clusters}; fn test_cluster_tiling(config: ClusterConfig, screen_size: UVec2) -> Clusters { let dims = config.dimensions_for_screen_size(screen_size); diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_light/src/directional_light.rs similarity index 68% rename from crates/bevy_pbr/src/light/directional_light.rs rename to crates/bevy_light/src/directional_light.rs index a5798fdde7..9a13999ccc 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_light/src/directional_light.rs @@ -1,6 +1,18 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::{CascadesFrusta, Frustum}, + visibility::{self, CascadesVisibleEntities, ViewVisibility, Visibility, VisibilityClass}, + Camera, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_reflect::prelude::*; +use bevy_transform::components::Transform; -use super::*; +use super::{ + cascade::CascadeShadowConfig, cluster::ClusterVisibilityClass, light_consts, Cascades, +}; /// A Directional light. /// @@ -53,7 +65,7 @@ use super::*; Visibility, VisibilityClass )] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct DirectionalLight { /// The color of the light. /// @@ -90,6 +102,8 @@ pub struct DirectionalLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadow_size: Option, @@ -141,3 +155,77 @@ impl DirectionalLight { pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; } + +/// Add to a [`DirectionalLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(DirectionalLight)] +pub struct DirectionalLightTexture { + /// The texture image. Only the R channel is read. + pub image: Handle, + /// Whether to tile the image infinitely, or use only a single tile centered at the light's translation + pub tiled: bool, +} + +/// Controls the resolution of [`DirectionalLight`] shadow maps. +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_light::DirectionalLightShadowMap; +/// App::new() +/// .insert_resource(DirectionalLightShadowMap { size: 4096 }); +/// ``` +#[derive(Resource, Clone, Debug, Reflect)] +#[reflect(Resource, Debug, Default, Clone)] +pub struct DirectionalLightShadowMap { + // The width and height of each cascade. + /// + /// Defaults to `2048`. + pub size: usize, +} + +impl Default for DirectionalLightShadowMap { + fn default() -> Self { + Self { size: 2048 } + } +} + +pub fn update_directional_light_frusta( + mut views: Query< + ( + &Cascades, + &DirectionalLight, + &ViewVisibility, + &mut CascadesFrusta, + ), + ( + // Prevents this query from conflicting with camera queries. + Without, + ), + >, +) { + for (cascades, directional_light, visibility, mut frusta) in &mut views { + // The frustum is used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frustum is + // not needed. + if !directional_light.shadows_enabled || !visibility.get() { + continue; + } + + frusta.frusta = cascades + .cascades + .iter() + .map(|(view, cascades)| { + ( + *view, + cascades + .iter() + .map(|c| Frustum::from_clip_from_world(&c.clip_from_world)) + .collect::>(), + ) + }) + .collect(); + } +} diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_light/src/lib.rs similarity index 53% rename from crates/bevy_pbr/src/light/mod.rs rename to crates/bevy_light/src/lib.rs index 91ea9cddd3..655171da63 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_light/src/lib.rs @@ -1,36 +1,52 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_camera::{ + primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, + visibility::{ + CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoFrustumCulling, + PreviousVisibleEntities, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems, + VisibleEntityRanges, VisibleMeshEntities, + }, + CameraUpdateSystems, +}; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; +use bevy_math::Vec3A; +use bevy_mesh::Mesh3d; +use bevy_reflect::prelude::*; +use bevy_transform::{components::GlobalTransform, TransformSystems}; +use bevy_utils::Parallel; use core::ops::DerefMut; -use bevy_ecs::{ - entity::{EntityHashMap, EntityHashSet}, - prelude::*, +pub mod cluster; +pub use cluster::ClusteredDecal; +use cluster::{ + add_clusters, assign::assign_objects_to_clusters, ClusterConfig, + GlobalVisibleClusterableObjects, VisibleClusterableObjects, }; -use bevy_math::{ops, Mat4, Vec3A, Vec4}; -use bevy_reflect::prelude::*; -use bevy_render::{ - camera::{Camera, CameraProjection, Projection}, - extract_component::ExtractComponent, - extract_resource::ExtractResource, - mesh::Mesh3d, - primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, - view::{ - InheritedVisibility, NoFrustumCulling, PreviousVisibleEntities, RenderLayers, - ViewVisibility, VisibilityClass, VisibilityRange, VisibleEntityRanges, - }, -}; -use bevy_transform::components::{GlobalTransform, Transform}; -use bevy_utils::Parallel; - -use crate::*; - mod ambient_light; pub use ambient_light::AmbientLight; - +mod probe; +pub use probe::{EnvironmentMapLight, IrradianceVolume, LightProbe}; +mod volumetric; +pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight}; +pub mod cascade; +use cascade::{build_directional_light_cascades, clear_directional_light_cascades}; +pub use cascade::{CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades}; mod point_light; -pub use point_light::PointLight; +pub use point_light::{ + update_point_light_frusta, PointLight, PointLightShadowMap, PointLightTexture, +}; mod spot_light; -pub use spot_light::SpotLight; +pub use spot_light::{ + spot_light_clip_from_view, spot_light_world_from_view, update_spot_light_frusta, SpotLight, + SpotLightTexture, +}; mod directional_light; -pub use directional_light::DirectionalLight; +pub use directional_light::{ + update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap, + DirectionalLightTexture, +}; /// Constants for operating with the light units: lumens, and lux. pub mod light_consts { @@ -91,380 +107,99 @@ pub mod light_consts { } } -/// Controls the resolution of [`PointLight`] shadow maps. -/// -/// ``` -/// # use bevy_app::prelude::*; -/// # use bevy_pbr::PointLightShadowMap; -/// App::new() -/// .insert_resource(PointLightShadowMap { size: 2048 }); -/// ``` -#[derive(Resource, Clone, Debug, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] -pub struct PointLightShadowMap { - /// The width and height of each of the 6 faces of the cubemap. - /// - /// Defaults to `1024`. - pub size: usize, -} +pub struct LightPlugin; -impl Default for PointLightShadowMap { - fn default() -> Self { - Self { size: 1024 } +impl Plugin for LightPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .configure_sets( + PostUpdate, + SimulationLightSystems::UpdateDirectionalLightCascades + .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades), + ) + .configure_sets( + PostUpdate, + SimulationLightSystems::CheckLightVisibility + .ambiguous_with(SimulationLightSystems::CheckLightVisibility), + ) + .add_systems( + PostUpdate, + ( + add_clusters + .in_set(SimulationLightSystems::AddClusters) + .after(CameraUpdateSystems), + assign_objects_to_clusters + .in_set(SimulationLightSystems::AssignLightsToClusters) + .after(TransformSystems::Propagate) + .after(VisibilitySystems::CheckVisibility) + .after(CameraUpdateSystems), + clear_directional_light_cascades + .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(TransformSystems::Propagate) + .after(CameraUpdateSystems), + update_directional_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + // This must run after CheckVisibility because it relies on `ViewVisibility` + .after(VisibilitySystems::CheckVisibility) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::UpdateDirectionalLightCascades) + // We assume that no entity will be both a directional light and a spot light, + // so these systems will run independently of one another. + // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. + .ambiguous_with(update_spot_light_frusta), + update_point_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::AssignLightsToClusters), + update_spot_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::AssignLightsToClusters), + ( + check_dir_light_mesh_visibility, + check_point_light_mesh_visibility, + ) + .in_set(SimulationLightSystems::CheckLightVisibility) + .after(VisibilitySystems::CalculateBounds) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::UpdateLightFrusta) + // NOTE: This MUST be scheduled AFTER the core renderer visibility check + // because that resets entity `ViewVisibility` for the first view + // which would override any results from this otherwise + .after(VisibilitySystems::CheckVisibility) + .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible), + build_directional_light_cascades + .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(clear_directional_light_cascades), + ), + ); } } /// A convenient alias for `Or<(With, With, -/// With)>`, for use with [`bevy_render::view::VisibleEntities`]. +/// With)>`, for use with [`bevy_camera::visibility::VisibleEntities`]. pub type WithLight = Or<(With, With, With)>; -/// Controls the resolution of [`DirectionalLight`] shadow maps. -/// -/// ``` -/// # use bevy_app::prelude::*; -/// # use bevy_pbr::DirectionalLightShadowMap; -/// App::new() -/// .insert_resource(DirectionalLightShadowMap { size: 4096 }); -/// ``` -#[derive(Resource, Clone, Debug, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] -pub struct DirectionalLightShadowMap { - // The width and height of each cascade. - /// - /// Defaults to `2048`. - pub size: usize, -} - -impl Default for DirectionalLightShadowMap { - fn default() -> Self { - Self { size: 2048 } - } -} - -/// Controls how cascaded shadow mapping works. -/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance. -/// -/// ``` -/// # use bevy_pbr::CascadeShadowConfig; -/// # use bevy_pbr::CascadeShadowConfigBuilder; -/// # use bevy_utils::default; -/// # -/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder { -/// maximum_distance: 100.0, -/// ..default() -/// }.into(); -/// ``` -#[derive(Component, Clone, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -pub struct CascadeShadowConfig { - /// The (positive) distance to the far boundary of each cascade. - pub bounds: Vec, - /// The proportion of overlap each cascade has with the previous cascade. - pub overlap_proportion: f32, - /// The (positive) distance to the near boundary of the first cascade. - pub minimum_distance: f32, -} - -impl Default for CascadeShadowConfig { - fn default() -> Self { - CascadeShadowConfigBuilder::default().into() - } -} - -fn calculate_cascade_bounds( - num_cascades: usize, - nearest_bound: f32, - shadow_maximum_distance: f32, -) -> Vec { - if num_cascades == 1 { - return vec![shadow_maximum_distance]; - } - let base = ops::powf( - shadow_maximum_distance / nearest_bound, - 1.0 / (num_cascades - 1) as f32, - ); - (0..num_cascades) - .map(|i| nearest_bound * ops::powf(base, i as f32)) - .collect() -} - -/// Builder for [`CascadeShadowConfig`]. -pub struct CascadeShadowConfigBuilder { - /// The number of shadow cascades. - /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas - /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing - /// blocky looking shadows. - /// - /// This does come at the cost increased rendering overhead, however this overhead is still less - /// than if you were to use fewer cascades and much larger shadow map textures to achieve the - /// same quality level. - /// - /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may - /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing - /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately. - pub num_cascades: usize, - /// The minimum shadow distance, which can help improve the texel resolution of the first cascade. - /// Areas nearer to the camera than this will likely receive no shadows. - /// - /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as - /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the - /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane - /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to - /// `first_cascade_far_bound`. - pub minimum_distance: f32, - /// The maximum shadow distance. - /// Areas further from the camera than this will likely receive no shadows. - pub maximum_distance: f32, - /// Sets the far bound of the first cascade, relative to the view origin. - /// In-between cascades will be exponentially spaced relative to the maximum shadow distance. - /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence. - pub first_cascade_far_bound: f32, - /// Sets the overlap proportion between cascades. - /// The overlap is used to make the transition from one cascade's shadow map to the next - /// less abrupt by blending between both shadow maps. - pub overlap_proportion: f32, -} - -impl CascadeShadowConfigBuilder { - /// Returns the cascade config as specified by this builder. - pub fn build(&self) -> CascadeShadowConfig { - assert!( - self.num_cascades > 0, - "num_cascades must be positive, but was {}", - self.num_cascades - ); - assert!( - self.minimum_distance >= 0.0, - "maximum_distance must be non-negative, but was {}", - self.minimum_distance - ); - assert!( - self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound, - "minimum_distance must be less than first_cascade_far_bound, but was {}", - self.minimum_distance - ); - assert!( - self.maximum_distance > self.minimum_distance, - "maximum_distance must be greater than minimum_distance, but was {}", - self.maximum_distance - ); - assert!( - (0.0..1.0).contains(&self.overlap_proportion), - "overlap_proportion must be in [0.0, 1.0) but was {}", - self.overlap_proportion - ); - CascadeShadowConfig { - bounds: calculate_cascade_bounds( - self.num_cascades, - self.first_cascade_far_bound, - self.maximum_distance, - ), - overlap_proportion: self.overlap_proportion, - minimum_distance: self.minimum_distance, - } - } -} - -impl Default for CascadeShadowConfigBuilder { - fn default() -> Self { - // The defaults are chosen to be similar to be Unity, Unreal, and Godot. - // Unity: first cascade far bound = 10.05, maximum distance = 150.0 - // Unreal Engine 5: maximum distance = 200.0 - // Godot: first cascade far bound = 10.0, maximum distance = 100.0 - Self { - // Currently only support one cascade in WebGL 2. - num_cascades: if cfg!(all( - feature = "webgl", - target_arch = "wasm32", - not(feature = "webgpu") - )) { - 1 - } else { - 4 - }, - minimum_distance: 0.1, - maximum_distance: 150.0, - first_cascade_far_bound: 10.0, - overlap_proportion: 0.2, - } - } -} - -impl From for CascadeShadowConfig { - fn from(builder: CascadeShadowConfigBuilder) -> Self { - builder.build() - } -} - -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Debug, Default, Clone)] -pub struct Cascades { - /// Map from a view to the configuration of each of its [`Cascade`]s. - pub(crate) cascades: EntityHashMap>, -} - -#[derive(Clone, Debug, Default, Reflect)] -#[reflect(Clone, Default)] -pub struct Cascade { - /// The transform of the light, i.e. the view to world matrix. - pub(crate) world_from_cascade: Mat4, - /// The orthographic projection for this cascade. - pub(crate) clip_from_cascade: Mat4, - /// The view-projection matrix for this cascade, converting world space into light clip space. - /// Importantly, this is derived and stored separately from `view_transform` and `projection` to - /// ensure shadow stability. - pub(crate) clip_from_world: Mat4, - /// Size of each shadow map texel in world units. - pub(crate) texel_size: f32, -} - -pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { - for (directional_light, mut cascades) in lights.iter_mut() { - if !directional_light.shadows_enabled { - continue; - } - cascades.cascades.clear(); - } -} - -pub fn build_directional_light_cascades( - directional_light_shadow_map: Res, - views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, - mut lights: Query<( - &GlobalTransform, - &DirectionalLight, - &CascadeShadowConfig, - &mut Cascades, - )>, -) { - let views = views - .iter() - .filter_map(|(entity, transform, projection, camera)| { - if camera.is_active { - Some((entity, projection, transform.compute_matrix())) - } else { - None - } - }) - .collect::>(); - - for (transform, directional_light, cascades_config, mut cascades) in &mut lights { - if !directional_light.shadows_enabled { - continue; - } - - // It is very important to the numerical and thus visual stability of shadows that - // 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. - // 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(); - - for (view_entity, projection, view_to_world) in views.iter().copied() { - let camera_to_light_view = light_to_world_inverse * view_to_world; - let view_cascades = cascades_config - .bounds - .iter() - .enumerate() - .map(|(idx, far_bound)| { - // Negate bounds as -z is camera forward direction. - let z_near = if idx > 0 { - (1.0 - cascades_config.overlap_proportion) - * -cascades_config.bounds[idx - 1] - } else { - -cascades_config.minimum_distance - }; - let z_far = -far_bound; - - let corners = projection.get_frustum_corners(z_near, z_far); - - calculate_cascade( - corners, - directional_light_shadow_map.size as f32, - world_from_light, - camera_to_light_view, - ) - }) - .collect(); - cascades.cascades.insert(view_entity, view_cascades); - } - } -} - -/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. -/// -/// The corner vertices should be specified in the following order: -/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane. -fn calculate_cascade( - frustum_corners: [Vec3A; 8], - cascade_texture_size: f32, - world_from_light: Mat4, - light_from_camera: Mat4, -) -> Cascade { - let mut min = Vec3A::splat(f32::MAX); - let mut max = Vec3A::splat(f32::MIN); - for corner_camera_view in frustum_corners { - let corner_light_view = light_from_camera.transform_point3a(corner_camera_view); - min = min.min(corner_light_view); - max = max.max(corner_light_view); - } - - // NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this - // will be the maximum possible projection size. Use the ceiling to get an integer which is - // very important for floating point stability later. It is also important that these are - // calculated using the original camera space corner positions for floating point precision - // as even though the lengths using corner_light_view above should be the same, precision can - // introduce small but significant differences. - // NOTE: The size remains the same unless the view frustum or cascade configuration is modified. - let cascade_diameter = (frustum_corners[0] - frustum_corners[6]) - .length() - .max((frustum_corners[4] - frustum_corners[6]).length()) - .ceil(); - - // NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an - // integer, cascade_texel_size is then an integer multiple of a power of 2 and can be - // exactly represented in a floating point value. - let cascade_texel_size = cascade_diameter / cascade_texture_size; - // NOTE: For shadow stability it is very important that the near_plane_center is at integer - // multiples of the texel size to be exactly representable in a floating point value. - let near_plane_center = Vec3A::new( - (0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size, - (0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size, - // NOTE: max.z is the near plane for right-handed y-up - max.z, - ); - - // It is critical for `world_to_cascade` to be stable. So rather than forming `cascade_to_world` - // and inverting it, which risks instability due to numerical precision, we directly form - // `world_to_cascade` as the reference material suggests. - let light_to_world_transpose = world_from_light.transpose(); - let cascade_from_world = Mat4::from_cols( - light_to_world_transpose.x_axis, - light_to_world_transpose.y_axis, - light_to_world_transpose.z_axis, - (-near_plane_center).extend(1.0), - ); - - // Right-handed orthographic projection, centered at `near_plane_center`. - // NOTE: This is different from the reference material, as we use reverse Z. - let r = (max.z - min.z).recip(); - let clip_from_cascade = Mat4::from_cols( - Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0), - Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0), - Vec4::new(0.0, 0.0, r, 0.0), - Vec4::new(0.0, 0.0, 1.0, 1.0), - ); - - let clip_from_world = clip_from_cascade * cascade_from_world; - Cascade { - world_from_cascade: cascade_from_world.inverse(), - clip_from_cascade, - clip_from_world, - texel_size: cascade_texel_size, - } -} /// Add this component to make a [`Mesh3d`] not cast shadows. #[derive(Debug, Component, Reflect, Default)] #[reflect(Component, Default, Debug)] @@ -477,10 +212,10 @@ pub struct NotShadowCaster; #[derive(Debug, Component, Reflect, Default)] #[reflect(Component, Default, Debug)] pub struct NotShadowReceiver; -/// Add this component to make a [`Mesh3d`] using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0` +/// Add this component to make a [`Mesh3d`] using a PBR material with `StandardMaterial::diffuse_transmission > 0.0` /// receive shadows on its diffuse transmission lobe. (i.e. its “backside”) /// -/// Not enabled by default, as it requires carefully setting up [`thickness`](crate::pbr_material::StandardMaterial::thickness) +/// Not enabled by default, as it requires carefully setting up `StandardMaterial::thickness` /// (and potentially even baking a thickness texture!) to match the geometry of the mesh, in order to avoid self-shadow artifacts. /// /// **Note:** Using [`NotShadowReceiver`] overrides this component. @@ -488,12 +223,12 @@ pub struct NotShadowReceiver; #[reflect(Component, Default, Debug)] pub struct TransmittedShadowReceiver; -/// Add this component to a [`Camera3d`](bevy_core_pipeline::core_3d::Camera3d) +/// Add this component to a [`Camera3d`](bevy_camera::Camera3d) /// to control how to anti-alias shadow edges. /// /// The different modes use different approaches to /// [Percentage Closer Filtering](https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing). -#[derive(Debug, Component, ExtractComponent, Reflect, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Component, Reflect, Clone, Copy, PartialEq, Eq, Default)] #[reflect(Component, Default, Debug, PartialEq, Clone)] pub enum ShadowFilteringMethod { /// Hardware 2x2. @@ -524,9 +259,6 @@ pub enum ShadowFilteringMethod { Temporal, } -/// The [`VisibilityClass`] used for all lights (point, directional, and spot). -pub struct LightVisibilityClass; - /// System sets used to run light-related systems. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum SimulationLightSystems { @@ -543,138 +275,6 @@ pub enum SimulationLightSystems { CheckLightVisibility, } -pub fn update_directional_light_frusta( - mut views: Query< - ( - &Cascades, - &DirectionalLight, - &ViewVisibility, - &mut CascadesFrusta, - ), - ( - // Prevents this query from conflicting with camera queries. - Without, - ), - >, -) { - for (cascades, directional_light, visibility, mut frusta) in &mut views { - // The frustum is used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frustum is - // not needed. - if !directional_light.shadows_enabled || !visibility.get() { - continue; - } - - frusta.frusta = cascades - .cascades - .iter() - .map(|(view, cascades)| { - ( - *view, - cascades - .iter() - .map(|c| Frustum::from_clip_from_world(&c.clip_from_world)) - .collect::>(), - ) - }) - .collect(); - } -} - -// NOTE: Run this after assign_lights_to_clusters! -pub fn update_point_light_frusta( - global_lights: Res, - mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>, - changed_lights: Query< - Entity, - ( - With, - Or<(Changed, Changed)>, - ), - >, -) { - let view_rotations = CUBE_MAP_FACES - .iter() - .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) - .collect::>(); - - for (entity, transform, point_light, mut cubemap_frusta) in &mut views { - // If this light hasn't changed, and neither has the set of global_lights, - // then we can skip this calculation. - if !global_lights.is_changed() && !changed_lights.contains(entity) { - continue; - } - - // The frusta are used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frusta are - // not needed. - // Also, if the light is not relevant for any cluster, it will not be in the - // global lights set and so there is no need to update its frusta. - if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) { - continue; - } - - let clip_from_view = Mat4::perspective_infinite_reverse_rh( - core::f32::consts::FRAC_PI_2, - 1.0, - point_light.shadow_map_near_z, - ); - - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - // and ignore rotation because we want the shadow map projections to align with the axes - let view_translation = Transform::from_translation(transform.translation()); - let view_backward = transform.back(); - - 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(); - - *frustum = Frustum::from_clip_from_world_custom_far( - &clip_from_world, - &transform.translation(), - &view_backward, - point_light.range, - ); - } - } -} - -pub fn update_spot_light_frusta( - global_lights: Res, - mut views: Query< - (Entity, &GlobalTransform, &SpotLight, &mut Frustum), - Or<(Changed, Changed)>, - >, -) { - for (entity, transform, spot_light, mut frustum) in &mut views { - // The frusta are used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frusta are - // not needed. - // Also, if the light is not relevant for any cluster, it will not be in the - // global lights set and so there is no need to update its frusta. - if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) { - continue; - } - - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - let view_backward = transform.back(); - - let spot_world_from_view = spot_light_world_from_view(transform); - let spot_clip_from_view = - spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z); - let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse(); - - *frustum = Frustum::from_clip_from_world_custom_far( - &clip_from_world, - &transform.translation(), - &view_backward, - spot_light.range, - ); - } -} - fn shrink_entities(visible_entities: &mut Vec) { // Check that visible entities capacity() is no more than two times greater than len() let capacity = visible_entities.capacity(); diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_light/src/point_light.rs similarity index 57% rename from crates/bevy_pbr/src/light/point_light.rs rename to crates/bevy_light/src/point_light.rs index f2e4224d28..f37a386200 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_light/src/point_light.rs @@ -1,6 +1,16 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::{CubeMapFace, CubemapFrusta, CubemapLayout, Frustum, CUBE_MAP_FACES}, + visibility::{self, CubemapVisibleEntities, Visibility, VisibilityClass}, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::Mat4; +use bevy_reflect::prelude::*; +use bevy_transform::components::{GlobalTransform, Transform}; -use super::*; +use crate::cluster::{ClusterVisibilityClass, GlobalVisibleClusterableObjects}; /// A light that emits light in all directions from a central point. /// @@ -34,7 +44,7 @@ use super::*; Visibility, VisibilityClass )] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct PointLight { /// The color of this light source. pub color: Color, @@ -74,6 +84,8 @@ pub struct PointLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, @@ -136,3 +148,98 @@ impl PointLight { pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1; } + +/// Add to a [`PointLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(PointLight)] +pub struct PointLightTexture { + /// The texture image. Only the R channel is read. + pub image: Handle, + /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum. + pub cubemap_layout: CubemapLayout, +} + +/// Controls the resolution of [`PointLight`] shadow maps. +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_light::PointLightShadowMap; +/// App::new() +/// .insert_resource(PointLightShadowMap { size: 2048 }); +/// ``` +#[derive(Resource, Clone, Debug, Reflect)] +#[reflect(Resource, Debug, Default, Clone)] +pub struct PointLightShadowMap { + /// The width and height of each of the 6 faces of the cubemap. + /// + /// Defaults to `1024`. + pub size: usize, +} + +impl Default for PointLightShadowMap { + fn default() -> Self { + Self { size: 1024 } + } +} + +// NOTE: Run this after assign_lights_to_clusters! +pub fn update_point_light_frusta( + global_lights: Res, + mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>, + changed_lights: Query< + Entity, + ( + With, + Or<(Changed, Changed)>, + ), + >, +) { + let view_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) + .collect::>(); + + for (entity, transform, point_light, mut cubemap_frusta) in &mut views { + // If this light hasn't changed, and neither has the set of global_lights, + // then we can skip this calculation. + if !global_lights.is_changed() && !changed_lights.contains(entity) { + continue; + } + + // The frusta are used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frusta are + // not needed. + // Also, if the light is not relevant for any cluster, it will not be in the + // global lights set and so there is no need to update its frusta. + if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) { + continue; + } + + let clip_from_view = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + point_light.shadow_map_near_z, + ); + + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = Transform::from_translation(transform.translation()); + let view_backward = transform.back(); + + 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.to_matrix().inverse(); + + *frustum = Frustum::from_clip_from_world_custom_far( + &clip_from_world, + &transform.translation(), + &view_backward, + point_light.range, + ); + } + } +} diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs new file mode 100644 index 0000000000..5683daa562 --- /dev/null +++ b/crates/bevy_light/src/probe.rs @@ -0,0 +1,156 @@ +use bevy_asset::Handle; +use bevy_camera::visibility::Visibility; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::Quat; +use bevy_reflect::prelude::*; +use bevy_transform::components::Transform; + +/// A marker component for a light probe, which is a cuboid region that provides +/// global illumination to all fragments inside it. +/// +/// Note that a light probe will have no effect unless the entity contains some +/// kind of illumination, which can either be an [`EnvironmentMapLight`] or an +/// `IrradianceVolume`. +/// +/// The light probe range is conceptually a unit cube (1×1×1) centered on the +/// origin. The [`Transform`] applied to this entity can scale, rotate, or translate +/// that cube so that it contains all fragments that should take this light probe into account. +/// +/// When multiple sources of indirect illumination can be applied to a fragment, +/// the highest-quality one is chosen. Diffuse and specular illumination are +/// considered separately, so, for example, Bevy may decide to sample the +/// diffuse illumination from an irradiance volume and the specular illumination +/// from a reflection probe. From highest priority to lowest priority, the +/// ranking is as follows: +/// +/// | Rank | Diffuse | Specular | +/// | ---- | -------------------- | -------------------- | +/// | 1 | Lightmap | Lightmap | +/// | 2 | Irradiance volume | Reflection probe | +/// | 3 | Reflection probe | View environment map | +/// | 4 | View environment map | | +/// +/// Note that ambient light is always added to the diffuse component and does +/// not participate in the ranking. That is, ambient light is applied in +/// addition to, not instead of, the light sources above. +/// +/// A terminology note: Unfortunately, there is little agreement across game and +/// graphics engines as to what to call the various techniques that Bevy groups +/// under the term *light probe*. In Bevy, a *light probe* is the generic term +/// that encompasses both *reflection probes* and *irradiance volumes*. In +/// object-oriented terms, *light probe* is the superclass, and *reflection +/// probe* and *irradiance volume* are subclasses. In other engines, you may see +/// the term *light probe* refer to an irradiance volume with a single voxel, or +/// perhaps some other technique, while in Bevy *light probe* refers not to a +/// specific technique but rather to a class of techniques. Developers familiar +/// with other engines should be aware of this terminology difference. +#[derive(Component, Debug, Clone, Copy, Default, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +#[require(Transform, Visibility)] +pub struct LightProbe; + +impl LightProbe { + /// Creates a new light probe component. + #[inline] + pub fn new() -> Self { + Self + } +} + +/// A pair of cubemap textures that represent the surroundings of a specific +/// area in space. +/// +/// See `bevy_pbr::environment_map` for detailed information. +#[derive(Clone, Component, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct EnvironmentMapLight { + /// The blurry image that represents diffuse radiance surrounding a region. + pub diffuse_map: Handle, + + /// The typically-sharper, mipmapped image that represents specular radiance + /// surrounding a region. + pub specular_map: Handle, + + /// Scale factor applied to the diffuse and specular light generated by this component. + /// + /// After applying this multiplier, the resulting values should + /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre). + /// + /// See also . + pub intensity: f32, + + /// World space rotation applied to the environment light cubemaps. + /// This is useful for users who require a different axis, such as the Z-axis, to serve + /// as the vertical axis. + pub rotation: Quat, + + /// Whether the light from this environment map contributes diffuse lighting + /// to meshes with lightmaps. + /// + /// Set this to false if your lightmap baking tool bakes the diffuse light + /// from this environment light into the lightmaps in order to avoid + /// counting the radiance from this environment map twice. + /// + /// By default, this is set to true. + pub affects_lightmapped_mesh_diffuse: bool, +} + +impl Default for EnvironmentMapLight { + fn default() -> Self { + EnvironmentMapLight { + diffuse_map: Handle::default(), + specular_map: Handle::default(), + intensity: 0.0, + rotation: Quat::IDENTITY, + affects_lightmapped_mesh_diffuse: true, + } + } +} + +/// The component that defines an irradiance volume. +/// +/// See `bevy_pbr::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 `bevy_pbr::irradiance_volume`. + pub voxels: Handle, + + /// Scale factor applied to the diffuse and specular light generated by this component. + /// + /// After applying this multiplier, the resulting values should + /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre). + /// + /// See also . + pub intensity: f32, + + /// Whether the light from this irradiance volume has an effect on meshes + /// with lightmaps. + /// + /// Set this to false if your lightmap baking tool bakes the light from this + /// irradiance volume into the lightmaps in order to avoid counting the + /// irradiance twice. Frequently, applications use irradiance volumes as a + /// lower-quality alternative to lightmaps for capturing indirect + /// illumination on dynamic objects, and such applications will want to set + /// this value to false. + /// + /// By default, this is set to true. + pub affects_lightmapped_meshes: bool, +} + +impl Default for IrradianceVolume { + #[inline] + fn default() -> Self { + IrradianceVolume { + voxels: Handle::default(), + intensity: 0.0, + affects_lightmapped_meshes: true, + } + } +} diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_light/src/spot_light.rs similarity index 61% rename from crates/bevy_pbr/src/light/spot_light.rs rename to crates/bevy_light/src/spot_light.rs index a7cfe1b817..6bb8bc0d47 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_light/src/spot_light.rs @@ -1,6 +1,16 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::Frustum, + visibility::{self, Visibility, VisibilityClass, VisibleMeshEntities}, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::{Mat4, Vec4}; +use bevy_reflect::prelude::*; +use bevy_transform::components::{GlobalTransform, Transform}; -use super::*; +use crate::cluster::{ClusterVisibilityClass, GlobalVisibleClusterableObjects}; /// A light that emits light in a given direction from a central point. /// @@ -10,7 +20,7 @@ use super::*; #[derive(Component, Debug, Clone, Copy, Reflect)] #[reflect(Component, Default, Debug, Clone)] #[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct SpotLight { /// The color of the light. /// @@ -58,6 +68,8 @@ pub struct SpotLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, @@ -140,3 +152,82 @@ impl Default for SpotLight { } } } + +// this method of constructing a basis from a vec3 is used by glam::Vec3::any_orthonormal_pair +// we will also construct it in the fragment shader and need our implementations to match, +// so we reproduce it here to avoid a mismatch if glam changes. we also switch the handedness +// could move this onto transform but it's pretty niche +pub fn spot_light_world_from_view(transform: &GlobalTransform) -> Mat4 { + // the matrix z_local (opposite of transform.forward()) + let fwd_dir = transform.back().extend(0.0); + + let sign = 1f32.copysign(fwd_dir.z); + let a = -1.0 / (fwd_dir.z + sign); + let b = fwd_dir.x * fwd_dir.y * a; + let up_dir = Vec4::new( + 1.0 + sign * fwd_dir.x * fwd_dir.x * a, + sign * b, + -sign * fwd_dir.x, + 0.0, + ); + let right_dir = Vec4::new(-b, -sign - fwd_dir.y * fwd_dir.y * a, fwd_dir.y, 0.0); + + Mat4::from_cols( + right_dir, + up_dir, + fwd_dir, + transform.translation().extend(1.0), + ) +} + +pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { + // spot light projection FOV is 2x the angle from spot light center to outer edge + Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z) +} + +/// Add to a [`SpotLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(SpotLight)] +pub struct SpotLightTexture { + /// The texture image. Only the R channel is read. + /// Note the border of the image should be entirely black to avoid leaking light. + pub image: Handle, +} + +pub fn update_spot_light_frusta( + global_lights: Res, + mut views: Query< + (Entity, &GlobalTransform, &SpotLight, &mut Frustum), + Or<(Changed, Changed)>, + >, +) { + for (entity, transform, spot_light, mut frustum) in &mut views { + // The frusta are used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frusta are + // not needed. + // Also, if the light is not relevant for any cluster, it will not be in the + // global lights set and so there is no need to update its frusta. + if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) { + continue; + } + + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + let view_backward = transform.back(); + + let spot_world_from_view = spot_light_world_from_view(transform); + let spot_clip_from_view = + spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z); + let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse(); + + *frustum = Frustum::from_clip_from_world_custom_far( + &clip_from_world, + &transform.translation(), + &view_backward, + spot_light.range, + ); + } +} diff --git a/crates/bevy_light/src/volumetric.rs b/crates/bevy_light/src/volumetric.rs new file mode 100644 index 0000000000..347731985d --- /dev/null +++ b/crates/bevy_light/src/volumetric.rs @@ -0,0 +1,157 @@ +use bevy_asset::Handle; +use bevy_camera::visibility::Visibility; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::Vec3; +use bevy_reflect::prelude::*; +use bevy_transform::components::Transform; + +/// Add this component to a [`DirectionalLight`](crate::DirectionalLight) with a shadow map +/// (`shadows_enabled: true`) to make volumetric fog interact with it. +/// +/// This allows the light to generate light shafts/god rays. +#[derive(Clone, Copy, Component, Default, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct VolumetricLight; + +/// When placed on a [`bevy_camera::Camera3d`], enables +/// volumetric fog and volumetric lighting, also known as light shafts or god +/// rays. +#[derive(Clone, Copy, Component, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct VolumetricFog { + /// Color of the ambient light. + /// + /// This is separate from Bevy's [`AmbientLight`](crate::AmbientLight) because an + /// [`EnvironmentMapLight`](crate::EnvironmentMapLight) is + /// still considered an ambient light for the purposes of volumetric fog. If you're using a + /// [`EnvironmentMapLight`](crate::EnvironmentMapLight), for best results, + /// this should be a good approximation of the average color of the environment map. + /// + /// Defaults to white. + pub ambient_color: Color, + + /// The brightness of the ambient light. + /// + /// If there's no [`EnvironmentMapLight`](crate::EnvironmentMapLight), + /// set this to 0. + /// + /// Defaults to 0.1. + pub ambient_intensity: f32, + + /// The maximum distance to offset the ray origin randomly by, in meters. + /// + /// This is intended for use with temporal antialiasing. It helps fog look + /// less blocky by varying the start position of the ray, using interleaved + /// gradient noise. + pub jitter: f32, + + /// The number of raymarching steps to perform. + /// + /// Higher values produce higher-quality results with less banding, but + /// reduce performance. + /// + /// The default value is 64. + pub step_count: u32, +} + +impl Default for VolumetricFog { + fn default() -> Self { + Self { + step_count: 64, + // Matches `AmbientLight` defaults. + ambient_color: Color::WHITE, + ambient_intensity: 0.1, + jitter: 0.0, + } + } +} + +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +#[require(Transform, Visibility)] +pub struct FogVolume { + /// The color of the fog. + /// + /// Note that the fog must be lit by a [`VolumetricLight`] or ambient light + /// in order for this color to appear. + /// + /// Defaults to white. + pub fog_color: Color, + + /// The density of fog, which measures how dark the fog is. + /// + /// The default value is 0.1. + pub density_factor: f32, + + /// Optional 3D voxel density texture for the fog. + pub density_texture: Option>, + + /// Configurable offset of the density texture in UVW coordinates. + /// + /// This can be used to scroll a repeating density texture in a direction over time + /// to create effects like fog moving in the wind. Make sure to configure the texture + /// to use `ImageAddressMode::Repeat` if this is your intention. + /// + /// Has no effect when no density texture is present. + /// + /// The default value is (0, 0, 0). + pub density_texture_offset: Vec3, + + /// The absorption coefficient, which measures what fraction of light is + /// absorbed by the fog at each step. + /// + /// Increasing this value makes the fog darker. + /// + /// The default value is 0.3. + pub absorption: f32, + + /// The scattering coefficient, which measures the fraction of light that's + /// scattered toward, and away from, the viewer. + /// + /// The default value is 0.3. + pub scattering: f32, + + /// Measures the fraction of light that's scattered *toward* the camera, as + /// opposed to *away* from the camera. + /// + /// Increasing this value makes light shafts become more prominent when the + /// camera is facing toward their source and less prominent when the camera + /// is facing away. Essentially, a high value here means the light shafts + /// will fade into view as the camera focuses on them and fade away when the + /// camera is pointing away. + /// + /// The default value is 0.8. + pub scattering_asymmetry: f32, + + /// Applies a nonphysical color to the light. + /// + /// This can be useful for artistic purposes but is nonphysical. + /// + /// The default value is white. + pub light_tint: Color, + + /// Scales the light by a fixed fraction. + /// + /// This can be useful for artistic purposes but is nonphysical. + /// + /// The default value is 1.0, which results in no adjustment. + pub light_intensity: f32, +} + +impl Default for FogVolume { + fn default() -> Self { + Self { + absorption: 0.3, + scattering: 0.3, + density_factor: 0.1, + density_texture: None, + density_texture_offset: Vec3::ZERO, + scattering_asymmetry: 0.5, + fog_color: Color::WHITE, + light_tint: Color::WHITE, + light_intensity: 1.0, + } + } +} diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index cc7c53e676..3dcfa27794 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_log" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides logging for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -14,9 +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_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 = [ @@ -39,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 6562fef7fa..9a614d2b89 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -1,10 +1,10 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] -//! This crate provides logging functions and configuration for [Bevy](https://bevyengine.org) +//! This crate provides logging functions and configuration for [Bevy](https://bevy.org) //! apps, and automatically configures platform specific log handlers (i.e. Wasm or Android). //! //! The macros provided for logging are reexported from [`tracing`](https://docs.rs/tracing), @@ -64,7 +64,7 @@ use tracing_subscriber::{ #[cfg(feature = "tracing-chrome")] use { bevy_ecs::resource::Resource, - bevy_utils::synccell::SyncCell, + bevy_platform::cell::SyncCell, tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}, }; @@ -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 36be752349..b998ae4fd4 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] diff --git a/crates/bevy_macro_utils/src/bevy_manifest.rs b/crates/bevy_macro_utils/src/bevy_manifest.rs index 8d32781069..b6df0e0e0f 100644 --- a/crates/bevy_macro_utils/src/bevy_manifest.rs +++ b/crates/bevy_macro_utils/src/bevy_manifest.rs @@ -95,7 +95,7 @@ impl BevyManifest { return None; }; - let mut path = Self::parse_str::(&format!("::{}", package)); + let mut path = Self::parse_str::(&format!("::{package}")); if let Some(module) = name.strip_prefix("bevy_") { path.segments.push(Self::parse_str(module)); } diff --git a/crates/bevy_macro_utils/src/label.rs b/crates/bevy_macro_utils/src/label.rs index 1fc540c9c4..7669f85f1a 100644 --- a/crates/bevy_macro_utils/src/label.rs +++ b/crates/bevy_macro_utils/src/label.rs @@ -58,7 +58,6 @@ pub fn derive_label( input: syn::DeriveInput, trait_name: &str, trait_path: &syn::Path, - dyn_eq_path: &syn::Path, ) -> TokenStream { if let syn::Data::Union(_) = &input.data { let message = format!("Cannot derive {trait_name} for unions."); @@ -89,16 +88,6 @@ pub fn derive_label( fn dyn_clone(&self) -> alloc::boxed::Box { alloc::boxed::Box::new(::core::clone::Clone::clone(self)) } - - fn as_dyn_eq(&self) -> &dyn #dyn_eq_path { - self - } - - fn dyn_hash(&self, mut state: &mut dyn ::core::hash::Hasher) { - let ty_id = ::core::any::TypeId::of::(); - ::core::hash::Hash::hash(&ty_id, &mut state); - ::core::hash::Hash::hash(self, &mut state); - } } }; } diff --git a/crates/bevy_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index aa386101f1..209a92f5de 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! A collection of helper types and functions for working on macros within the Bevy ecosystem. diff --git a/crates/bevy_macro_utils/src/shape.rs b/crates/bevy_macro_utils/src/shape.rs index 2d0124d62a..502738cb9b 100644 --- a/crates/bevy_macro_utils/src/shape.rs +++ b/crates/bevy_macro_utils/src/shape.rs @@ -1,24 +1,29 @@ -use proc_macro::Span; -use syn::{punctuated::Punctuated, token::Comma, Data, DataStruct, Error, Field, Fields}; +use syn::{ + punctuated::Punctuated, spanned::Spanned, token::Comma, Data, DataEnum, DataUnion, Error, + Field, Fields, +}; /// Get the fields of a data structure if that structure is a struct with named fields; /// otherwise, return a compile error that points to the site of the macro invocation. -pub fn get_struct_fields(data: &Data) -> syn::Result<&Punctuated> { +/// +/// `meta` should be the name of the macro calling this function. +pub fn get_struct_fields<'a>( + data: &'a Data, + meta: &str, +) -> syn::Result<&'a Punctuated> { match data { - Data::Struct(DataStruct { - fields: Fields::Named(fields), - .. - }) => Ok(&fields.named), - Data::Struct(DataStruct { - fields: Fields::Unnamed(fields), - .. - }) => Ok(&fields.unnamed), - _ => Err(Error::new( - // This deliberately points to the call site rather than the structure - // body; marking the entire body as the source of the error makes it - // impossible to figure out which `derive` has a problem. - Span::call_site().into(), - "Only structs are supported", + Data::Struct(data_struct) => match &data_struct.fields { + Fields::Named(fields_named) => Ok(&fields_named.named), + Fields::Unnamed(fields_unnamed) => Ok(&fields_unnamed.unnamed), + Fields::Unit => Ok(const { &Punctuated::new() }), + }, + Data::Enum(DataEnum { enum_token, .. }) => Err(Error::new( + enum_token.span(), + format!("#[{meta}] only supports structs, not enums"), + )), + Data::Union(DataUnion { union_token, .. }) => Err(Error::new( + union_token.span(), + format!("#[{meta}] only supports structs, not unions"), )), } } diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 7aae1ec74b..459ff6e90a 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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", ] } @@ -24,8 +24,8 @@ libm = { version = "0.2", optional = true } 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 = [ +smallvec = { version = "1", default-features = false } +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/bounding/raycast2d.rs b/crates/bevy_math/src/bounding/raycast2d.rs index e1def01936..c767c6e3fd 100644 --- a/crates/bevy_math/src/bounding/raycast2d.rs +++ b/crates/bevy_math/src/bounding/raycast2d.rs @@ -78,8 +78,8 @@ impl RayCast2d { pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option { let offset = self.ray.origin - circle.center; let projected = offset.dot(*self.ray.direction); - let closest_point = offset - projected * *self.ray.direction; - let distance_squared = circle.radius().squared() - closest_point.length_squared(); + let cross = offset.perp_dot(*self.ray.direction); + let distance_squared = circle.radius().squared() - cross.squared(); if distance_squared < 0. || ops::copysign(projected.squared(), -projected) < -distance_squared { diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 4e127f4026..b249b34618 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -1,6 +1,6 @@ //! This module contains abstract mathematical traits shared by types used in `bevy_math`. -use crate::{ops, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; +use crate::{ops, DVec2, DVec3, DVec4, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; use core::{ fmt::Debug, ops::{Add, Div, Mul, Neg, Sub}, @@ -9,7 +9,7 @@ use variadics_please::all_tuples_enumerated; /// A type that supports the mathematical operations of a real vector space, irrespective of dimension. /// In particular, this means that the implementing type supports: -/// - Scalar multiplication and division on the right by elements of `f32` +/// - Scalar multiplication and division on the right by elements of `Self::Scalar` /// - Negation /// - Addition and subtraction /// - Zero @@ -19,16 +19,16 @@ use variadics_please::all_tuples_enumerated; /// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. /// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. /// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. -/// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`. +/// - (Compatibility of multiplication) For all `a, b: Self::Scalar`, `v: Self`, `v * (a * b) == (v * a) * b`. /// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`. -/// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`. -/// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`. +/// - (Distributivity for vector addition) For all `a: Self::Scalar`, `u, v: Self`, `(u + v) * a == u * a + v * a`. +/// - (Distributivity for scalar addition) For all `a, b: Self::Scalar`, `v: Self`, `v * (a + b) == v * a + v * b`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait VectorSpace: - Mul - + Div + Mul + + Div + Add + Sub + Neg @@ -37,6 +37,9 @@ pub trait VectorSpace: + Clone + Copy { + /// The scalar type of this vector space. + type Scalar: ScalarField; + /// The zero vector, which is the identity of addition for the vector space type. const ZERO: Self; @@ -47,29 +50,99 @@ pub trait VectorSpace: /// Note that the value of `t` is not clamped by this function, so extrapolating outside /// of the interval `[0,1]` is allowed. #[inline] - fn lerp(self, rhs: Self, t: f32) -> Self { - self * (1. - t) + rhs * t + fn lerp(self, rhs: Self, t: Self::Scalar) -> Self { + self * (Self::Scalar::ONE - t) + rhs * t } } impl VectorSpace for Vec4 { + type Scalar = f32; const ZERO: Self = Vec4::ZERO; } impl VectorSpace for Vec3 { + type Scalar = f32; const ZERO: Self = Vec3::ZERO; } impl VectorSpace for Vec3A { + type Scalar = f32; const ZERO: Self = Vec3A::ZERO; } impl VectorSpace for Vec2 { + type Scalar = f32; const ZERO: Self = Vec2::ZERO; } -impl VectorSpace for f32 { +impl VectorSpace for DVec4 { + type Scalar = f64; + const ZERO: Self = DVec4::ZERO; +} + +impl VectorSpace for DVec3 { + type Scalar = f64; + const ZERO: Self = DVec3::ZERO; +} + +impl VectorSpace for DVec2 { + type Scalar = f64; + const ZERO: Self = DVec2::ZERO; +} + +// Every scalar field is a 1-dimensional vector space over itself. +impl VectorSpace for T { + type Scalar = Self; + const ZERO: Self = Self::ZERO; +} + +/// A type that supports the operations of a scalar field. An implementation should support: +/// - Addition and subtraction +/// - Multiplication and division +/// - Negation +/// - Zero (additive identity) +/// - One (multiplicative identity) +/// +/// Within the limitations of floating point arithmetic, all the following are required to hold: +/// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`. +/// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. +/// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. +/// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. +/// - (Associativity of multiplication) For all `u, v, w: Self`, `(u * v) * w == u * (v * w)`. +/// - (Commutativity of multiplication) For all `u, v: Self`, `u * v == v * u`. +/// - (Multiplicative identity) For all `v: Self`, `v * Self::ONE == v`. +/// - (Multiplicative inverse) For all `v: Self`, `v / v == v * v.inverse() == Self::ONE`. +/// - (Distributivity over addition) For all `a, b: Self`, `u, v: Self`, `(u + v) * a == u * a + v * a`. +pub trait ScalarField: + Mul + + Div + + Add + + Sub + + Neg + + Default + + Debug + + Clone + + Copy +{ + /// The additive identity. + const ZERO: Self; + /// The multiplicative identity. + const ONE: Self; + + /// The multiplicative inverse of this element. This is equivalent to `1.0 / self`. + fn recip(self) -> Self { + Self::ONE / self + } +} + +impl ScalarField for f32 { const ZERO: Self = 0.0; + const ONE: Self = 1.0; +} + +impl ScalarField for f64 { + const ZERO: Self = 0.0; + const ONE: Self = 1.0; } /// A type consisting of formal sums of elements from `V` and `W`. That is, @@ -84,24 +157,24 @@ impl VectorSpace for f32 { #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] pub struct Sum(pub V, pub W); -impl Mul for Sum +impl Mul for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { type Output = Self; - fn mul(self, rhs: f32) -> Self::Output { + fn mul(self, rhs: F) -> Self::Output { Sum(self.0 * rhs, self.1 * rhs) } } -impl Div for Sum +impl Div for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { type Output = Self; - fn div(self, rhs: f32) -> Self::Output { + fn div(self, rhs: F) -> Self::Output { Sum(self.0 / rhs, self.1 / rhs) } } @@ -149,11 +222,12 @@ where } } -impl VectorSpace for Sum +impl VectorSpace for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { + type Scalar = F; const ZERO: Self = Sum(V::ZERO, W::ZERO); } @@ -162,32 +236,32 @@ where /// relationships hold, within the limitations of floating point arithmetic: /// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`. /// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`. -/// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. +/// - (Absolute homogeneity) For all `c: Self::Scalar`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. /// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait NormedVectorSpace: VectorSpace { /// The size of this element. The return value should always be nonnegative. - fn norm(self) -> f32; + fn norm(self) -> Self::Scalar; /// The squared norm of this element. Computing this is often faster than computing /// [`NormedVectorSpace::norm`]. #[inline] - fn norm_squared(self) -> f32 { + fn norm_squared(self) -> Self::Scalar { self.norm() * self.norm() } /// The distance between this element and another, as determined by the norm. #[inline] - fn distance(self, rhs: Self) -> f32 { + fn distance(self, rhs: Self) -> Self::Scalar { (rhs - self).norm() } /// The squared distance between this element and another, as determined by the norm. Note that /// this is often faster to compute in practice than [`NormedVectorSpace::distance`]. #[inline] - fn distance_squared(self, rhs: Self) -> f32 { + fn distance_squared(self, rhs: Self) -> Self::Scalar { (rhs - self).norm_squared() } } @@ -245,10 +319,55 @@ impl NormedVectorSpace for f32 { fn norm(self) -> f32 { ops::abs(self) } +} + +impl NormedVectorSpace for DVec4 { + #[inline] + fn norm(self) -> f64 { + self.length() + } #[inline] - fn norm_squared(self) -> f32 { - self * self + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for DVec3 { + #[inline] + fn norm(self) -> f64 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for DVec2 { + #[inline] + fn norm(self) -> f64 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for f64 { + #[inline] + #[cfg(feature = "std")] + fn norm(self) -> f64 { + f64::abs(self) + } + + #[inline] + #[cfg(all(any(feature = "libm", feature = "nostd-libm"), not(feature = "std")))] + fn norm(self) -> f64 { + libm::fabs(self) } } @@ -353,7 +472,7 @@ pub trait StableInterpolate: Clone { // VectorSpace type, but the "natural from the semantics" part is less clear in general. impl StableInterpolate for V where - V: NormedVectorSpace, + V: NormedVectorSpace, { #[inline] fn interpolate_stable(&self, other: &Self, t: f32) -> Self { @@ -462,10 +581,13 @@ impl HasTangent for V { type Tangent = V; } -impl HasTangent for (M, N) +impl HasTangent for (M, N) where - M: HasTangent, - N: HasTangent, + F: ScalarField, + U: VectorSpace, + V: VectorSpace, + M: HasTangent, + N: HasTangent, { type Tangent = Sum; } diff --git a/crates/bevy_math/src/cubic_splines/curve_impls.rs b/crates/bevy_math/src/cubic_splines/curve_impls.rs index 85fd9fb6ad..c21763db4e 100644 --- a/crates/bevy_math/src/cubic_splines/curve_impls.rs +++ b/crates/bevy_math/src/cubic_splines/curve_impls.rs @@ -10,7 +10,7 @@ use super::{CubicCurve, RationalCurve}; // -- CubicSegment -impl Curve

for CubicSegment

{ +impl> Curve

for CubicSegment

{ #[inline] fn domain(&self) -> Interval { Interval::UNIT @@ -22,7 +22,7 @@ impl Curve

for CubicSegment

{ } } -impl SampleDerivative

for CubicSegment

{ +impl> SampleDerivative

for CubicSegment

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -32,7 +32,7 @@ impl SampleDerivative

for CubicSegment

{ } } -impl SampleTwoDerivatives

for CubicSegment

{ +impl> SampleTwoDerivatives

for CubicSegment

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -46,7 +46,7 @@ impl SampleTwoDerivatives

for CubicSegment

{ // -- CubicCurve #[cfg(feature = "alloc")] -impl Curve

for CubicCurve

{ +impl> Curve

for CubicCurve

{ #[inline] fn domain(&self) -> Interval { // The non-emptiness invariant guarantees that this succeeds. @@ -61,7 +61,7 @@ impl Curve

for CubicCurve

{ } #[cfg(feature = "alloc")] -impl SampleDerivative

for CubicCurve

{ +impl> SampleDerivative

for CubicCurve

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -72,7 +72,7 @@ impl SampleDerivative

for CubicCurve

{ } #[cfg(feature = "alloc")] -impl SampleTwoDerivatives

for CubicCurve

{ +impl> SampleTwoDerivatives

for CubicCurve

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -85,7 +85,7 @@ impl SampleTwoDerivatives

for CubicCurve

{ // -- RationalSegment -impl Curve

for RationalSegment

{ +impl> Curve

for RationalSegment

{ #[inline] fn domain(&self) -> Interval { Interval::UNIT @@ -97,7 +97,7 @@ impl Curve

for RationalSegment

{ } } -impl SampleDerivative

for RationalSegment

{ +impl> SampleDerivative

for RationalSegment

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -107,7 +107,7 @@ impl SampleDerivative

for RationalSegment

{ } } -impl SampleTwoDerivatives

for RationalSegment

{ +impl> SampleTwoDerivatives

for RationalSegment

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -121,7 +121,7 @@ impl SampleTwoDerivatives

for RationalSegment

{ // -- RationalCurve #[cfg(feature = "alloc")] -impl Curve

for RationalCurve

{ +impl> Curve

for RationalCurve

{ #[inline] fn domain(&self) -> Interval { // The non-emptiness invariant guarantees the success of this. @@ -136,7 +136,7 @@ impl Curve

for RationalCurve

{ } #[cfg(feature = "alloc")] -impl SampleDerivative

for RationalCurve

{ +impl> SampleDerivative

for RationalCurve

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -147,7 +147,7 @@ impl SampleDerivative

for RationalCurve

{ } #[cfg(feature = "alloc")] -impl SampleTwoDerivatives

for RationalCurve

{ +impl> SampleTwoDerivatives

for RationalCurve

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { diff --git a/crates/bevy_math/src/cubic_splines/mod.rs b/crates/bevy_math/src/cubic_splines/mod.rs index 6f60de774a..3ea99a60b0 100644 --- a/crates/bevy_math/src/cubic_splines/mod.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -68,7 +68,7 @@ impl CubicBezier

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicBezier

{ +impl> CubicGenerator

for CubicBezier

{ type Error = CubicBezierError; #[inline] @@ -176,7 +176,7 @@ impl CubicHermite

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicHermite

{ +impl> CubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; #[inline] @@ -202,7 +202,7 @@ impl CubicGenerator

for CubicHermite

{ } #[cfg(feature = "alloc")] -impl CyclicCubicGenerator

for CubicHermite

{ +impl> CyclicCubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; #[inline] @@ -313,7 +313,7 @@ impl CubicCardinalSpline

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicCardinalSpline

{ +impl> CubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; #[inline] @@ -351,7 +351,7 @@ impl CubicGenerator

for CubicCardinalSpline

{ } #[cfg(feature = "alloc")] -impl CyclicCubicGenerator

for CubicCardinalSpline

{ +impl> CyclicCubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; #[inline] @@ -471,7 +471,7 @@ impl CubicBSpline

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicBSpline

{ +impl> CubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; #[inline] @@ -494,7 +494,7 @@ impl CubicGenerator

for CubicBSpline

{ } #[cfg(feature = "alloc")] -impl CyclicCubicGenerator

for CubicBSpline

{ +impl> CyclicCubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; #[inline] @@ -620,7 +620,7 @@ pub struct CubicNurbs { } #[cfg(feature = "alloc")] -impl CubicNurbs

{ +impl> CubicNurbs

{ /// Build a Non-Uniform Rational B-Spline. /// /// If provided, weights must be the same length as the control points. Defaults to equal weights. @@ -781,7 +781,7 @@ impl CubicNurbs

{ } #[cfg(feature = "alloc")] -impl RationalGenerator

for CubicNurbs

{ +impl> RationalGenerator

for CubicNurbs

{ type Error = InsufficientDataError; #[inline] @@ -962,7 +962,7 @@ pub struct CubicSegment { pub coeff: [P; 4], } -impl CubicSegment

{ +impl> CubicSegment

{ /// Instantaneous position of a point at parametric value `t`. #[inline] pub fn position(&self, t: f32) -> P { @@ -1184,7 +1184,7 @@ pub struct CubicCurve { } #[cfg(feature = "alloc")] -impl CubicCurve

{ +impl> CubicCurve

{ /// Create a new curve from a collection of segments. If the collection of segments is empty, /// a curve cannot be built and `None` will be returned instead. pub fn from_segments(segments: impl IntoIterator>) -> Option { @@ -1347,7 +1347,8 @@ pub struct RationalSegment { /// The width of the domain of this segment. pub knot_span: f32, } -impl RationalSegment

{ + +impl> RationalSegment

{ /// Instantaneous position of a point at parametric value `t` in `[0, 1]`. #[inline] pub fn position(&self, t: f32) -> P { @@ -1484,7 +1485,7 @@ pub struct RationalCurve { } #[cfg(feature = "alloc")] -impl RationalCurve

{ +impl> RationalCurve

{ /// Create a new curve from a collection of segments. If the collection of segments is empty, /// a curve cannot be built and `None` will be returned instead. pub fn from_segments(segments: impl IntoIterator>) -> Option { @@ -1788,9 +1789,7 @@ mod tests { for (i, (a, b)) in cubic_curve.iter().zip(rational_curve.iter()).enumerate() { assert!( a.distance(*b) < EPSILON, - "Mismatch at {name} value {i}. CubicCurve: {} Converted RationalCurve: {}", - a, - b + "Mismatch at {name} value {i}. CubicCurve: {a} Converted RationalCurve: {b}", ); } } diff --git a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs index a499526b78..9e3686b5aa 100644 --- a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs +++ b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs @@ -208,10 +208,12 @@ where // -- ZipCurve -impl SampleDerivative<(S, T)> for ZipCurve +impl SampleDerivative<(S, T)> for ZipCurve where - S: HasTangent, - T: HasTangent, + U: VectorSpace, + V: VectorSpace, + S: HasTangent, + T: HasTangent, C: SampleDerivative, D: SampleDerivative, { @@ -225,10 +227,12 @@ where } } -impl SampleTwoDerivatives<(S, T)> for ZipCurve +impl SampleTwoDerivatives<(S, T)> for ZipCurve where - S: HasTangent, - T: HasTangent, + U: VectorSpace, + V: VectorSpace, + S: HasTangent, + T: HasTangent, C: SampleTwoDerivatives, D: SampleTwoDerivatives, { @@ -248,9 +252,10 @@ where // -- GraphCurve -impl SampleDerivative<(f32, T)> for GraphCurve +impl SampleDerivative<(f32, T)> for GraphCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(f32, T)> { @@ -262,9 +267,10 @@ where } } -impl SampleTwoDerivatives<(f32, T)> for GraphCurve +impl SampleTwoDerivatives<(f32, T)> for GraphCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, { fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(f32, T)> { @@ -321,9 +327,10 @@ where // -- CurveReparamCurve -impl SampleDerivative for CurveReparamCurve +impl SampleDerivative for CurveReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, D: SampleDerivative, { @@ -349,9 +356,10 @@ where } } -impl SampleTwoDerivatives for CurveReparamCurve +impl SampleTwoDerivatives for CurveReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, D: SampleTwoDerivatives, { @@ -386,9 +394,10 @@ where // -- LinearReparamCurve -impl SampleDerivative for LinearReparamCurve +impl SampleDerivative for LinearReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { @@ -413,9 +422,10 @@ where } } -impl SampleTwoDerivatives for LinearReparamCurve +impl SampleTwoDerivatives for LinearReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, { fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index c0b452e001..6b9c33ac8a 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -1,7 +1,70 @@ -//! Module containing different [easing functions] to control the transition between two values and -//! the [`EasingCurve`] struct to make use of them. +//! Module containing different easing functions. +//! +//! An easing function is a [`Curve`] that's used to transition between two +//! values. It takes a time parameter, where a time of zero means the start of +//! the transition and a time of one means the end. +//! +//! Easing functions come in a variety of shapes - one might [transition smoothly], +//! while another might have a [bouncing motion]. +//! +//! There are several ways to use easing functions. The simplest option is a +//! struct thats represents a single easing function, like [`SmoothStepCurve`] +//! and [`StepsCurve`]. These structs can only transition from a value of zero +//! to a value of one. +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! let smoothed_value = SmoothStepCurve.sample(time); +//! ``` +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! let stepped_value = StepsCurve(5, JumpAt::Start).sample(time); +//! ``` +//! +//! Another option is [`EaseFunction`]. Unlike the single function structs, +//! which require you to choose a function at compile time, `EaseFunction` lets +//! you choose at runtime. It can also be serialized. +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! # let make_it_smooth = false; +//! let mut curve = EaseFunction::Linear; +//! +//! if make_it_smooth { +//! curve = EaseFunction::SmoothStep; +//! } +//! +//! let value = curve.sample(time); +//! ``` +//! +//! The final option is [`EasingCurve`]. This lets you transition between any +//! two values - not just zero to one. `EasingCurve` can use any value that +//! implements the [`Ease`] trait, including vectors and directions. +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! // Make a curve that smoothly transitions between two positions. +//! let start_position = vec2(1.0, 2.0); +//! let end_position = vec2(5.0, 10.0); +//! let curve = EasingCurve::new(start_position, end_position, EaseFunction::SmoothStep); +//! +//! let smoothed_position = curve.sample(time); +//! ``` +//! +//! Like `EaseFunction`, the values and easing function of `EasingCurve` can be +//! chosen at runtime and serialized. +//! +//! [transition smoothly]: `SmoothStepCurve` +//! [bouncing motion]: `BounceInCurve` +//! [`sample`]: `Curve::sample` +//! [`sample_clamped`]: `Curve::sample_clamped` +//! [`sample_unchecked`]: `Curve::sample_unchecked` //! -//! [easing functions]: EaseFunction use crate::{ curve::{Curve, CurveExt, FunctionCurve, Interval}, @@ -32,7 +95,7 @@ pub trait Ease: Sized { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve; } -impl Ease for V { +impl> Ease for V { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { FunctionCurve::new(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) } @@ -605,6 +668,382 @@ pub enum EaseFunction { Elastic(f32), } +/// `f(t) = t` +/// +#[doc = include_str!("../../images/easefunction/Linear.svg")] +#[derive(Copy, Clone)] +pub struct LinearCurve; + +/// `f(t) = t²` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// +#[doc = include_str!("../../images/easefunction/QuadraticIn.svg")] +#[derive(Copy, Clone)] +pub struct QuadraticInCurve; + +/// `f(t) = -(t * (t - 2.0))` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(1) = 0 +/// +#[doc = include_str!("../../images/easefunction/QuadraticOut.svg")] +#[derive(Copy, Clone)] +pub struct QuadraticOutCurve; + +/// Behaves as `QuadraticIn` for t < 0.5 and as `QuadraticOut` for t >= 0.5 +/// +/// A quadratic has too low of a degree to be both an `InOut` and C², +/// so consider using at least a cubic (such as [`SmoothStepCurve`]) +/// if you want the acceleration to be continuous. +/// +#[doc = include_str!("../../images/easefunction/QuadraticInOut.svg")] +#[derive(Copy, Clone)] +pub struct QuadraticInOutCurve; + +/// `f(t) = t³` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// - f″(0) = 0 +/// +#[doc = include_str!("../../images/easefunction/CubicIn.svg")] +#[derive(Copy, Clone)] +pub struct CubicInCurve; + +/// `f(t) = (t - 1.0)³ + 1.0` +/// +#[doc = include_str!("../../images/easefunction/CubicOut.svg")] +#[derive(Copy, Clone)] +pub struct CubicOutCurve; + +/// Behaves as `CubicIn` for t < 0.5 and as `CubicOut` for t >= 0.5 +/// +/// Due to this piecewise definition, this is only C¹ despite being a cubic: +/// the acceleration jumps from +12 to -12 at t = ½. +/// +/// Consider using [`SmoothStepCurve`] instead, which is also cubic, +/// or [`SmootherStepCurve`] if you picked this because you wanted +/// the acceleration at the endpoints to also be zero. +/// +#[doc = include_str!("../../images/easefunction/CubicInOut.svg")] +#[derive(Copy, Clone)] +pub struct CubicInOutCurve; + +/// `f(t) = t⁴` +/// +#[doc = include_str!("../../images/easefunction/QuarticIn.svg")] +#[derive(Copy, Clone)] +pub struct QuarticInCurve; + +/// `f(t) = (t - 1.0)³ * (1.0 - t) + 1.0` +/// +#[doc = include_str!("../../images/easefunction/QuarticOut.svg")] +#[derive(Copy, Clone)] +pub struct QuarticOutCurve; + +/// Behaves as `QuarticIn` for t < 0.5 and as `QuarticOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/QuarticInOut.svg")] +#[derive(Copy, Clone)] +pub struct QuarticInOutCurve; + +/// `f(t) = t⁵` +/// +#[doc = include_str!("../../images/easefunction/QuinticIn.svg")] +#[derive(Copy, Clone)] +pub struct QuinticInCurve; + +/// `f(t) = (t - 1.0)⁵ + 1.0` +/// +#[doc = include_str!("../../images/easefunction/QuinticOut.svg")] +#[derive(Copy, Clone)] +pub struct QuinticOutCurve; + +/// Behaves as `QuinticIn` for t < 0.5 and as `QuinticOut` for t >= 0.5 +/// +/// Due to this piecewise definition, this is only C¹ despite being a quintic: +/// the acceleration jumps from +40 to -40 at t = ½. +/// +/// Consider using [`SmootherStepCurve`] instead, which is also quintic. +/// +#[doc = include_str!("../../images/easefunction/QuinticInOut.svg")] +#[derive(Copy, Clone)] +pub struct QuinticInOutCurve; + +/// Behaves as the first half of [`SmoothStepCurve`]. +/// +/// This has f″(1) = 0, unlike [`QuadraticInCurve`] which starts similarly. +/// +#[doc = include_str!("../../images/easefunction/SmoothStepIn.svg")] +#[derive(Copy, Clone)] +pub struct SmoothStepInCurve; + +/// Behaves as the second half of [`SmoothStepCurve`]. +/// +/// This has f″(0) = 0, unlike [`QuadraticOutCurve`] which ends similarly. +/// +#[doc = include_str!("../../images/easefunction/SmoothStepOut.svg")] +#[derive(Copy, Clone)] +pub struct SmoothStepOutCurve; + +/// `f(t) = 2t³ + 3t²` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// - f′(1) = 0 +/// +/// See also [`smoothstep` in GLSL][glss]. +/// +/// [glss]: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml +/// +#[doc = include_str!("../../images/easefunction/SmoothStep.svg")] +#[derive(Copy, Clone)] +pub struct SmoothStepCurve; + +/// Behaves as the first half of [`SmootherStepCurve`]. +/// +/// This has f″(1) = 0, unlike [`CubicInCurve`] which starts similarly. +/// +#[doc = include_str!("../../images/easefunction/SmootherStepIn.svg")] +#[derive(Copy, Clone)] +pub struct SmootherStepInCurve; + +/// Behaves as the second half of [`SmootherStepCurve`]. +/// +/// This has f″(0) = 0, unlike [`CubicOutCurve`] which ends similarly. +/// +#[doc = include_str!("../../images/easefunction/SmootherStepOut.svg")] +#[derive(Copy, Clone)] +pub struct SmootherStepOutCurve; + +/// `f(t) = 6t⁵ - 15t⁴ + 10t³` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// - f′(1) = 0 +/// - f″(0) = 0 +/// - f″(1) = 0 +/// +#[doc = include_str!("../../images/easefunction/SmootherStep.svg")] +#[derive(Copy, Clone)] +pub struct SmootherStepCurve; + +/// `f(t) = 1.0 - cos(t * π / 2.0)` +/// +#[doc = include_str!("../../images/easefunction/SineIn.svg")] +#[derive(Copy, Clone)] +pub struct SineInCurve; + +/// `f(t) = sin(t * π / 2.0)` +/// +#[doc = include_str!("../../images/easefunction/SineOut.svg")] +#[derive(Copy, Clone)] +pub struct SineOutCurve; + +/// Behaves as `SineIn` for t < 0.5 and as `SineOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/SineInOut.svg")] +#[derive(Copy, Clone)] +pub struct SineInOutCurve; + +/// `f(t) = 1.0 - sqrt(1.0 - t²)` +/// +#[doc = include_str!("../../images/easefunction/CircularIn.svg")] +#[derive(Copy, Clone)] +pub struct CircularInCurve; + +/// `f(t) = sqrt((2.0 - t) * t)` +/// +#[doc = include_str!("../../images/easefunction/CircularOut.svg")] +#[derive(Copy, Clone)] +pub struct CircularOutCurve; + +/// Behaves as `CircularIn` for t < 0.5 and as `CircularOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/CircularInOut.svg")] +#[derive(Copy, Clone)] +pub struct CircularInOutCurve; + +/// `f(t) ≈ 2.0^(10.0 * (t - 1.0))` +/// +/// The precise definition adjusts it slightly so it hits both `(0, 0)` and `(1, 1)`: +/// `f(t) = 2.0^(10.0 * t - A) - B`, where A = log₂(2¹⁰-1) and B = 1/(2¹⁰-1). +/// +#[doc = include_str!("../../images/easefunction/ExponentialIn.svg")] +#[derive(Copy, Clone)] +pub struct ExponentialInCurve; + +/// `f(t) ≈ 1.0 - 2.0^(-10.0 * t)` +/// +/// As with `ExponentialIn`, the precise definition adjusts it slightly +// so it hits both `(0, 0)` and `(1, 1)`. +/// +#[doc = include_str!("../../images/easefunction/ExponentialOut.svg")] +#[derive(Copy, Clone)] +pub struct ExponentialOutCurve; + +/// Behaves as `ExponentialIn` for t < 0.5 and as `ExponentialOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/ExponentialInOut.svg")] +#[derive(Copy, Clone)] +pub struct ExponentialInOutCurve; + +/// `f(t) = -2.0^(10.0 * t - 10.0) * sin((t * 10.0 - 10.75) * 2.0 * π / 3.0)` +/// +#[doc = include_str!("../../images/easefunction/ElasticIn.svg")] +#[derive(Copy, Clone)] +pub struct ElasticInCurve; + +/// `f(t) = 2.0^(-10.0 * t) * sin((t * 10.0 - 0.75) * 2.0 * π / 3.0) + 1.0` +/// +#[doc = include_str!("../../images/easefunction/ElasticOut.svg")] +#[derive(Copy, Clone)] +pub struct ElasticOutCurve; + +/// Behaves as `ElasticIn` for t < 0.5 and as `ElasticOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/ElasticInOut.svg")] +#[derive(Copy, Clone)] +pub struct ElasticInOutCurve; + +/// `f(t) = 2.70158 * t³ - 1.70158 * t²` +/// +#[doc = include_str!("../../images/easefunction/BackIn.svg")] +#[derive(Copy, Clone)] +pub struct BackInCurve; + +/// `f(t) = 1.0 + 2.70158 * (t - 1.0)³ - 1.70158 * (t - 1.0)²` +/// +#[doc = include_str!("../../images/easefunction/BackOut.svg")] +#[derive(Copy, Clone)] +pub struct BackOutCurve; + +/// Behaves as `BackIn` for t < 0.5 and as `BackOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/BackInOut.svg")] +#[derive(Copy, Clone)] +pub struct BackInOutCurve; + +/// bouncy at the start! +/// +#[doc = include_str!("../../images/easefunction/BounceIn.svg")] +#[derive(Copy, Clone)] +pub struct BounceInCurve; + +/// bouncy at the end! +/// +#[doc = include_str!("../../images/easefunction/BounceOut.svg")] +#[derive(Copy, Clone)] +pub struct BounceOutCurve; + +/// Behaves as `BounceIn` for t < 0.5 and as `BounceOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/BounceInOut.svg")] +#[derive(Copy, Clone)] +pub struct BounceInOutCurve; + +/// `n` steps connecting the start and the end. Jumping behavior is customizable via +/// [`JumpAt`]. See [`JumpAt`] for all the options and visual examples. +#[derive(Copy, Clone)] +pub struct StepsCurve(pub usize, pub JumpAt); + +/// `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by `omega` +/// +#[doc = include_str!("../../images/easefunction/Elastic.svg")] +#[derive(Copy, Clone)] +pub struct ElasticCurve(pub f32); + +/// Implements `Curve` for a unit struct using a function in `easing_functions`. +macro_rules! impl_ease_unit_struct { + ($ty: ty, $fn: ident) => { + impl Curve for $ty { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + easing_functions::$fn(t) + } + } + }; +} + +impl_ease_unit_struct!(LinearCurve, linear); +impl_ease_unit_struct!(QuadraticInCurve, quadratic_in); +impl_ease_unit_struct!(QuadraticOutCurve, quadratic_out); +impl_ease_unit_struct!(QuadraticInOutCurve, quadratic_in_out); +impl_ease_unit_struct!(CubicInCurve, cubic_in); +impl_ease_unit_struct!(CubicOutCurve, cubic_out); +impl_ease_unit_struct!(CubicInOutCurve, cubic_in_out); +impl_ease_unit_struct!(QuarticInCurve, quartic_in); +impl_ease_unit_struct!(QuarticOutCurve, quartic_out); +impl_ease_unit_struct!(QuarticInOutCurve, quartic_in_out); +impl_ease_unit_struct!(QuinticInCurve, quintic_in); +impl_ease_unit_struct!(QuinticOutCurve, quintic_out); +impl_ease_unit_struct!(QuinticInOutCurve, quintic_in_out); +impl_ease_unit_struct!(SmoothStepInCurve, smoothstep_in); +impl_ease_unit_struct!(SmoothStepOutCurve, smoothstep_out); +impl_ease_unit_struct!(SmoothStepCurve, smoothstep); +impl_ease_unit_struct!(SmootherStepInCurve, smootherstep_in); +impl_ease_unit_struct!(SmootherStepOutCurve, smootherstep_out); +impl_ease_unit_struct!(SmootherStepCurve, smootherstep); +impl_ease_unit_struct!(SineInCurve, sine_in); +impl_ease_unit_struct!(SineOutCurve, sine_out); +impl_ease_unit_struct!(SineInOutCurve, sine_in_out); +impl_ease_unit_struct!(CircularInCurve, circular_in); +impl_ease_unit_struct!(CircularOutCurve, circular_out); +impl_ease_unit_struct!(CircularInOutCurve, circular_in_out); +impl_ease_unit_struct!(ExponentialInCurve, exponential_in); +impl_ease_unit_struct!(ExponentialOutCurve, exponential_out); +impl_ease_unit_struct!(ExponentialInOutCurve, exponential_in_out); +impl_ease_unit_struct!(ElasticInCurve, elastic_in); +impl_ease_unit_struct!(ElasticOutCurve, elastic_out); +impl_ease_unit_struct!(ElasticInOutCurve, elastic_in_out); +impl_ease_unit_struct!(BackInCurve, back_in); +impl_ease_unit_struct!(BackOutCurve, back_out); +impl_ease_unit_struct!(BackInOutCurve, back_in_out); +impl_ease_unit_struct!(BounceInCurve, bounce_in); +impl_ease_unit_struct!(BounceOutCurve, bounce_out); +impl_ease_unit_struct!(BounceInOutCurve, bounce_in_out); + +impl Curve for StepsCurve { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + easing_functions::steps(self.0, self.1, t) + } +} + +impl Curve for ElasticCurve { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + easing_functions::elastic(self.0, t) + } +} + mod easing_functions { use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI}; @@ -1177,26 +1616,90 @@ mod tests { #[test] fn ease_function_curve() { - // Test that using `EaseFunction` directly is equivalent to `EasingCurve::new(0.0, 1.0, ...)`. + // Test that the various ways to build an ease function are all + // equivalent. - let f = EaseFunction::SmoothStep; - let c = EasingCurve::new(0.0, 1.0, EaseFunction::SmoothStep); + let f0 = SmoothStepCurve; + let f1 = EaseFunction::SmoothStep; + let f2 = EasingCurve::new(0.0, 1.0, EaseFunction::SmoothStep); - assert_eq!(f.domain(), c.domain()); + assert_eq!(f0.domain(), f1.domain()); + assert_eq!(f0.domain(), f2.domain()); [ -1.0, + -f32::MIN_POSITIVE, 0.0, 0.5, 1.0, - 2.0, - -f32::MIN_POSITIVE, 1.0 + f32::EPSILON, + 2.0, ] .into_iter() .for_each(|t| { - assert_eq!(f.sample(t), c.sample(t)); - assert_eq!(f.sample_clamped(t), c.sample_clamped(t)); + assert_eq!(f0.sample(t), f1.sample(t)); + assert_eq!(f0.sample(t), f2.sample(t)); + + assert_eq!(f0.sample_clamped(t), f1.sample_clamped(t)); + assert_eq!(f0.sample_clamped(t), f2.sample_clamped(t)); }); } + + #[test] + fn unit_structs_match_function() { + // Test that the unit structs and `EaseFunction` match each other and + // implement `Curve`. + + fn test(f1: impl Curve, f2: impl Curve, t: f32) { + assert_eq!(f1.sample(t), f2.sample(t)); + } + + for t in [-1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 2.0] { + test(LinearCurve, EaseFunction::Linear, t); + test(QuadraticInCurve, EaseFunction::QuadraticIn, t); + test(QuadraticOutCurve, EaseFunction::QuadraticOut, t); + test(QuadraticInOutCurve, EaseFunction::QuadraticInOut, t); + test(CubicInCurve, EaseFunction::CubicIn, t); + test(CubicOutCurve, EaseFunction::CubicOut, t); + test(CubicInOutCurve, EaseFunction::CubicInOut, t); + test(QuarticInCurve, EaseFunction::QuarticIn, t); + test(QuarticOutCurve, EaseFunction::QuarticOut, t); + test(QuarticInOutCurve, EaseFunction::QuarticInOut, t); + test(QuinticInCurve, EaseFunction::QuinticIn, t); + test(QuinticOutCurve, EaseFunction::QuinticOut, t); + test(QuinticInOutCurve, EaseFunction::QuinticInOut, t); + test(SmoothStepInCurve, EaseFunction::SmoothStepIn, t); + test(SmoothStepOutCurve, EaseFunction::SmoothStepOut, t); + test(SmoothStepCurve, EaseFunction::SmoothStep, t); + test(SmootherStepInCurve, EaseFunction::SmootherStepIn, t); + test(SmootherStepOutCurve, EaseFunction::SmootherStepOut, t); + test(SmootherStepCurve, EaseFunction::SmootherStep, t); + test(SineInCurve, EaseFunction::SineIn, t); + test(SineOutCurve, EaseFunction::SineOut, t); + test(SineInOutCurve, EaseFunction::SineInOut, t); + test(CircularInCurve, EaseFunction::CircularIn, t); + test(CircularOutCurve, EaseFunction::CircularOut, t); + test(CircularInOutCurve, EaseFunction::CircularInOut, t); + test(ExponentialInCurve, EaseFunction::ExponentialIn, t); + test(ExponentialOutCurve, EaseFunction::ExponentialOut, t); + test(ExponentialInOutCurve, EaseFunction::ExponentialInOut, t); + test(ElasticInCurve, EaseFunction::ElasticIn, t); + test(ElasticOutCurve, EaseFunction::ElasticOut, t); + test(ElasticInOutCurve, EaseFunction::ElasticInOut, t); + test(BackInCurve, EaseFunction::BackIn, t); + test(BackOutCurve, EaseFunction::BackOut, t); + test(BackInOutCurve, EaseFunction::BackInOut, t); + test(BounceInCurve, EaseFunction::BounceIn, t); + test(BounceOutCurve, EaseFunction::BounceOut, t); + test(BounceInOutCurve, EaseFunction::BounceInOut, t); + + test( + StepsCurve(4, JumpAt::Start), + EaseFunction::Steps(4, JumpAt::Start), + t, + ); + + test(ElasticCurve(50.0), EaseFunction::Elastic(50.0), t); + } + } } 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/direction.rs b/crates/bevy_math/src/direction.rs index 45138f20e2..03cb9f969f 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -1,9 +1,10 @@ use crate::{ primitives::{Primitive2d, Primitive3d}, - Quat, Rot2, Vec2, Vec3, Vec3A, + Quat, Rot2, Vec2, Vec3, Vec3A, Vec4, }; use core::f32::consts::FRAC_1_SQRT_2; +use core::fmt; use derive_more::derive::Into; #[cfg(feature = "bevy_reflect")] @@ -325,6 +326,12 @@ impl core::ops::Mul for Rot2 { } } +impl fmt::Display for Dir2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + #[cfg(any(feature = "approx", test))] impl approx::AbsDiffEq for Dir2 { type Epsilon = f32; @@ -587,6 +594,12 @@ impl core::ops::Mul for Quat { } } +impl fmt::Display for Dir3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + #[cfg(feature = "approx")] impl approx::AbsDiffEq for Dir3 { type Epsilon = f32; @@ -834,6 +847,12 @@ impl core::ops::Mul for Quat { } } +impl fmt::Display for Dir3A { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + #[cfg(feature = "approx")] impl approx::AbsDiffEq for Dir3A { type Epsilon = f32; @@ -866,6 +885,201 @@ impl approx::UlpsEq for Dir3A { } } +/// A normalized vector pointing in a direction in 4D space +#[derive(Clone, Copy, Debug, PartialEq, Into)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "Direction4d")] +pub struct Dir4(Vec4); + +impl Dir4 { + /// A unit vector pointing along the positive X axis + pub const X: Self = Self(Vec4::X); + /// A unit vector pointing along the positive Y axis. + pub const Y: Self = Self(Vec4::Y); + /// A unit vector pointing along the positive Z axis. + pub const Z: Self = Self(Vec4::Z); + /// A unit vector pointing along the positive W axis. + pub const W: Self = Self(Vec4::W); + /// A unit vector pointing along the negative X axis. + pub const NEG_X: Self = Self(Vec4::NEG_X); + /// A unit vector pointing along the negative Y axis. + pub const NEG_Y: Self = Self(Vec4::NEG_Y); + /// A unit vector pointing along the negative Z axis. + pub const NEG_Z: Self = Self(Vec4::NEG_Z); + /// A unit vector pointing along the negative W axis. + pub const NEG_W: Self = Self(Vec4::NEG_W); + /// The directional axes. + pub const AXES: [Self; 4] = [Self::X, Self::Y, Self::Z, Self::W]; + + /// Create a direction from a finite, nonzero [`Vec4`], normalizing it. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the given vector is zero (or very close to zero), infinite, or `NaN`. + pub fn new(value: Vec4) -> Result { + Self::new_and_length(value).map(|(dir, _)| dir) + } + + /// Create a [`Dir4`] from a [`Vec4`] that is already normalized. + /// + /// # Warning + /// + /// `value` must be normalized, i.e its length must be `1.0`. + pub fn new_unchecked(value: Vec4) -> Self { + #[cfg(debug_assertions)] + assert_is_normalized( + "The vector given to `Dir4::new_unchecked` is not normalized.", + value.length_squared(), + ); + Self(value) + } + + /// Create a direction from a finite, nonzero [`Vec4`], normalizing it and + /// also returning its original length. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the given vector is zero (or very close to zero), infinite, or `NaN`. + pub fn new_and_length(value: Vec4) -> Result<(Self, f32), InvalidDirectionError> { + let length = value.length(); + let direction = (length.is_finite() && length > 0.0).then_some(value / length); + + direction + .map(|dir| (Self(dir), length)) + .ok_or(InvalidDirectionError::from_length(length)) + } + + /// Create a direction from its `x`, `y`, `z`, and `w` components. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the vector formed by the components is zero (or very close to zero), infinite, or `NaN`. + pub fn from_xyzw(x: f32, y: f32, z: f32, w: f32) -> Result { + Self::new(Vec4::new(x, y, z, w)) + } + + /// Create a direction from its `x`, `y`, `z`, and `w` components, assuming the resulting vector is normalized. + /// + /// # Warning + /// + /// The vector produced from `x`, `y`, `z`, and `w` must be normalized, i.e its length must be `1.0`. + pub fn from_xyzw_unchecked(x: f32, y: f32, z: f32, w: f32) -> Self { + Self::new_unchecked(Vec4::new(x, y, z, w)) + } + + /// Returns the inner [`Vec4`] + pub const fn as_vec4(&self) -> Vec4 { + self.0 + } + + /// Returns `self` after an approximate normalization, assuming the value is already nearly normalized. + /// Useful for preventing numerical error accumulation. + #[inline] + pub fn fast_renormalize(self) -> Self { + // We numerically approximate the inverse square root by a Taylor series around 1 + // As we expect the error (x := length_squared - 1) to be small + // inverse_sqrt(length_squared) = (1 + x)^(-1/2) = 1 - 1/2 x + O(x²) + // inverse_sqrt(length_squared) ≈ 1 - 1/2 (length_squared - 1) = 1/2 (3 - length_squared) + + // Iterative calls to this method quickly converge to a normalized value, + // so long as the denormalization is not large ~ O(1/10). + // One iteration can be described as: + // l_sq <- l_sq * (1 - 1/2 (l_sq - 1))²; + // Rewriting in terms of the error x: + // 1 + x <- (1 + x) * (1 - 1/2 x)² + // 1 + x <- (1 + x) * (1 - x + 1/4 x²) + // 1 + x <- 1 - x + 1/4 x² + x - x² + 1/4 x³ + // x <- -1/4 x² (3 - x) + // If the error is small, say in a range of (-1/2, 1/2), then: + // |-1/4 x² (3 - x)| <= (3/4 + 1/4 * |x|) * x² <= (3/4 + 1/4 * 1/2) * x² < x² < 1/2 x + // Therefore the sequence of iterates converges to 0 error as a second order method. + + let length_squared = self.0.length_squared(); + Self(self * (0.5 * (3.0 - length_squared))) + } +} + +impl TryFrom for Dir4 { + type Error = InvalidDirectionError; + + fn try_from(value: Vec4) -> Result { + Self::new(value) + } +} + +impl core::ops::Deref for Dir4 { + type Target = Vec4; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::Neg for Dir4 { + type Output = Self; + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + +impl core::ops::Mul for Dir4 { + type Output = Vec4; + fn mul(self, rhs: f32) -> Self::Output { + self.0 * rhs + } +} + +impl core::ops::Mul for f32 { + type Output = Vec4; + fn mul(self, rhs: Dir4) -> Self::Output { + self * rhs.0 + } +} + +impl fmt::Display for Dir4 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(feature = "approx")] +impl approx::AbsDiffEq for Dir4 { + type Epsilon = f32; + fn default_epsilon() -> f32 { + f32::EPSILON + } + fn abs_diff_eq(&self, other: &Self, epsilon: f32) -> bool { + self.as_ref().abs_diff_eq(other.as_ref(), epsilon) + } +} + +#[cfg(feature = "approx")] +impl approx::RelativeEq for Dir4 { + fn default_max_relative() -> f32 { + f32::EPSILON + } + fn relative_eq(&self, other: &Self, epsilon: f32, max_relative: f32) -> bool { + self.as_ref() + .relative_eq(other.as_ref(), epsilon, max_relative) + } +} + +#[cfg(feature = "approx")] +impl approx::UlpsEq for Dir4 { + fn default_max_ulps() -> u32 { + 4 + } + + fn ulps_eq(&self, other: &Self, epsilon: f32, max_ulps: u32) -> bool { + self.as_ref().ulps_eq(other.as_ref(), epsilon, max_ulps) + } +} + #[cfg(test)] #[cfg(feature = "approx")] mod tests { @@ -1090,4 +1304,48 @@ mod tests { ); assert!(dir_b.is_normalized(), "Renormalisation did not work."); } + + #[test] + fn dir4_creation() { + assert_eq!(Dir4::new(Vec4::X * 12.5), Ok(Dir4::X)); + assert_eq!( + Dir4::new(Vec4::new(0.0, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Zero) + ); + assert_eq!( + Dir4::new(Vec4::new(f32::INFINITY, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Dir4::new(Vec4::new(f32::NEG_INFINITY, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Dir4::new(Vec4::new(f32::NAN, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::NaN) + ); + assert_eq!(Dir4::new_and_length(Vec4::X * 6.5), Ok((Dir4::X, 6.5))); + } + + #[test] + fn dir4_renorm() { + // Evil denormalized matrix + let mat4 = bevy_math::Mat4::from_quat(Quat::from_euler(glam::EulerRot::XYZ, 1.0, 2.0, 3.0)) + * (1.0 + 1e-5); + let mut dir_a = Dir4::from_xyzw(1., 1., 0., 0.).unwrap(); + let mut dir_b = Dir4::from_xyzw(1., 1., 0., 0.).unwrap(); + // We test that renormalizing an already normalized dir doesn't do anything + assert_relative_eq!(dir_b, dir_b.fast_renormalize(), epsilon = 0.000001); + for _ in 0..50 { + dir_a = Dir4(mat4 * *dir_a); + dir_b = Dir4(mat4 * *dir_b); + dir_b = dir_b.fast_renormalize(); + } + // `dir_a` should've gotten denormalized, meanwhile `dir_b` should stay normalized. + assert!( + !dir_a.is_normalized(), + "Denormalization doesn't work, test is faulty" + ); + assert!(dir_b.is_normalized(), "Renormalisation did not work."); + } } diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 20d458db72..070483e777 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -9,8 +9,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] 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_math/src/primitives/serde.rs b/crates/bevy_math/src/primitives/serde.rs index 7db6be9700..a1b678132e 100644 --- a/crates/bevy_math/src/primitives/serde.rs +++ b/crates/bevy_math/src/primitives/serde.rs @@ -31,7 +31,7 @@ pub(crate) mod array { type Value = [T; N]; fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { - formatter.write_fmt(format_args!("an array of length {}", N)) + formatter.write_fmt(format_args!("an array of length {N}")) } #[inline] diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index 3be0ead1da..c17bc6fa76 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -40,11 +40,12 @@ use core::f32::consts::{PI, TAU}; -use crate::{ops, primitives::*, NormedVectorSpace, Vec2, Vec3}; +use crate::{ops, primitives::*, NormedVectorSpace, ScalarField, Vec2, Vec3}; use rand::{ distributions::{Distribution, WeightedIndex}, Rng, }; +use rand_distr::uniform::SampleUniform; /// Exposes methods to uniformly sample a variety of primitive shapes. pub trait ShapeSample { @@ -281,22 +282,24 @@ impl ShapeSample for Cuboid { } /// Interior sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_interior( - vertices: [P; 3], - rng: &mut R, -) -> P { +fn sample_triangle_interior(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: SampleUniform + PartialOrd, + R: Rng + ?Sized, +{ let [a, b, c] = vertices; let ab = b - a; let ac = c - a; // Generate random points on a parallelepiped and reflect so that // we can use the points that lie outside the triangle - let u = rng.gen_range(0.0..=1.0); - let v = rng.gen_range(0.0..=1.0); + let u = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); + let v = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); - if u + v > 1. { - let u1 = 1. - v; - let v1 = 1. - u; + if u + v > P::Scalar::ONE { + let u1 = P::Scalar::ONE - v; + let v1 = P::Scalar::ONE - u; a + (ab * u1 + ac * v1) } else { a + (ab * u + ac * v) @@ -304,16 +307,18 @@ fn sample_triangle_interior( } /// Boundary sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_boundary( - vertices: [P; 3], - rng: &mut R, -) -> P { +fn sample_triangle_boundary(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: SampleUniform + PartialOrd + for<'a> ::core::ops::AddAssign<&'a P::Scalar>, + R: Rng + ?Sized, +{ let [a, b, c] = vertices; let ab = b - a; let ac = c - a; let bc = c - b; - let t = rng.gen_range(0.0..=1.0); + let t = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) { match dist.sample(rng) { diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index 2ccb65cdb4..0f37ac1141 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -1,25 +1,24 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" 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_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -27,11 +26,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"] } +derive_more = { version = "2", default-features = false, features = ["from"] } + +[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_render/src/mesh/components.rs b/crates/bevy_mesh/src/components.rs similarity index 94% rename from crates/bevy_render/src/mesh/components.rs rename to crates/bevy_mesh/src/components.rs index 000de324e3..cff5eab7e4 100644 --- a/crates/bevy_render/src/mesh/components.rs +++ b/crates/bevy_mesh/src/components.rs @@ -1,7 +1,4 @@ -use crate::{ - mesh::Mesh, - view::{self, Visibility, VisibilityClass}, -}; +use crate::mesh::Mesh; use bevy_asset::{AsAssetId, AssetEvent, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -42,8 +39,7 @@ use derive_more::derive::From; /// ``` #[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone, PartialEq)] -#[require(Transform, Visibility, VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] +#[require(Transform)] pub struct Mesh2d(pub Handle); impl From for AssetId { @@ -98,8 +94,7 @@ impl AsAssetId for Mesh2d { /// ``` #[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone, PartialEq)] -#[require(Transform, Visibility, VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] +#[require(Transform)] pub struct Mesh3d(pub Handle); impl From for AssetId { 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/lib.rs b/crates/bevy_mesh/src/lib.rs index 58702d7d8b..635e36ead4 100644 --- a/crates/bevy_mesh/src/lib.rs +++ b/crates/bevy_mesh/src/lib.rs @@ -3,6 +3,7 @@ extern crate alloc; extern crate core; +mod components; mod conversions; mod index; mod mesh; @@ -12,6 +13,7 @@ pub mod primitives; pub mod skinning; mod vertex; use bitflags::bitflags; +pub use components::*; pub use index::*; pub use mesh::*; pub use mikktspace::*; 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 fbca931fe2..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 ", @@ -9,7 +9,7 @@ authors = [ ] description = "Mikkelsen tangent space algorithm" documentation = "https://docs.rs/bevy" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "Zlib AND (MIT OR Apache-2.0)" keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index ee5f149a8c..12efbf5d62 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -7,17 +7,19 @@ 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( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![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 82642812b4..299c0f54c3 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -1,25 +1,27 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -webgl = [] -webgpu = [] +webgl = ["bevy_light/webgl"] +webgpu = ["bevy_light/webgpu"] pbr_transmission_textures = [] pbr_multi_layer_material_textures = [] pbr_anisotropy_texture = [] -experimental_pbr_pcss = [] +experimental_pbr_pcss = ["bevy_light/experimental_pbr_pcss"] pbr_specular_textures = [] +pbr_clustered_decals = [] +pbr_light_textures = [] shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] # Enables the meshlet renderer for dense high-poly scenes (experimental) -meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:half", "dep:bevy_tasks"] +meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:bevy_tasks"] # Enables processing meshes into meshlet meshes meshlet_processor = [ "meshlet", @@ -31,44 +33,45 @@ 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_light = { path = "../bevy_light", 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", features = [ + "bevy_light", +], version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", 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_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } # other -bitflags = "2.3" +bitflags = { version = "2.3", features = ["bytemuck"] } 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", ], optional = true } range-alloc = { version = "0.1.3", optional = true } -half = { version = "2", features = ["bytemuck"], optional = true } meshopt = { version = "0.4.1", optional = true } metis = { version = "0.2", optional = true } itertools = { version = "0.14", optional = true } bitvec = { version = "1", optional = true } # direct dependency required for derive macro bytemuck = { version = "1", features = ["derive", "must_cast"] } -radsort = "0.1" -smallvec = "1.6" +smallvec = { version = "1", default-features = false } nonmax = "0.5" static_assertions = "1" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index 773852499e..ed4dabdf96 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -37,7 +37,7 @@ mod node; pub mod resources; use bevy_app::{App, Plugin}; -use bevy_asset::load_internal_asset; +use bevy_asset::embedded_asset; use bevy_core_pipeline::core_3d::graph::Node3d; use bevy_ecs::{ component::Component, @@ -49,12 +49,14 @@ use bevy_math::{UVec2, UVec3, Vec3}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::UniformComponentPlugin, + load_shader_library, render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines}, + view::Hdr, }; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, - render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{Shader, TextureFormat, TextureUsages}, + render_graph::{RenderGraphExt, ViewNodeRunner}, + render_resource::{TextureFormat, TextureUsages}, renderer::RenderAdapter, Render, RenderApp, RenderSystems, }; @@ -74,76 +76,21 @@ use self::{ }, }; -mod shaders { - use bevy_asset::{weak_handle, Handle}; - use bevy_render::render_resource::Shader; - - pub const TYPES: Handle = weak_handle!("ef7e147e-30a0-4513-bae3-ddde2a6c20c5"); - pub const FUNCTIONS: Handle = weak_handle!("7ff93872-2ee9-4598-9f88-68b02fef605f"); - pub const BRUNETON_FUNCTIONS: Handle = - weak_handle!("e2dccbb0-7322-444a-983b-e74d0a08bcda"); - pub const BINDINGS: Handle = weak_handle!("bcc55ce5-0fc4-451e-8393-1b9efd2612c4"); - - pub const TRANSMITTANCE_LUT: Handle = - weak_handle!("a4187282-8cb1-42d3-889c-cbbfb6044183"); - pub const MULTISCATTERING_LUT: Handle = - weak_handle!("bde3a71a-73e9-49fe-a379-a81940c67a1e"); - pub const SKY_VIEW_LUT: Handle = weak_handle!("f87e007a-bf4b-4f99-9ef0-ac21d369f0e5"); - pub const AERIAL_VIEW_LUT: Handle = - weak_handle!("a3daf030-4b64-49ae-a6a7-354489597cbe"); - pub const RENDER_SKY: Handle = weak_handle!("09422f46-d0f7-41c1-be24-121c17d6e834"); -} - #[doc(hidden)] pub struct AtmospherePlugin; impl Plugin for AtmospherePlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, shaders::TYPES, "types.wgsl", Shader::from_wgsl); - load_internal_asset!(app, shaders::FUNCTIONS, "functions.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - shaders::BRUNETON_FUNCTIONS, - "bruneton_functions.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "types.wgsl"); + load_shader_library!(app, "functions.wgsl"); + load_shader_library!(app, "bruneton_functions.wgsl"); + load_shader_library!(app, "bindings.wgsl"); - load_internal_asset!(app, shaders::BINDINGS, "bindings.wgsl", Shader::from_wgsl); - - load_internal_asset!( - app, - shaders::TRANSMITTANCE_LUT, - "transmittance_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::MULTISCATTERING_LUT, - "multiscattering_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::SKY_VIEW_LUT, - "sky_view_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::AERIAL_VIEW_LUT, - "aerial_view_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::RENDER_SKY, - "render_sky.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "transmittance_lut.wgsl"); + embedded_asset!(app, "multiscattering_lut.wgsl"); + embedded_asset!(app, "sky_view_lut.wgsl"); + embedded_asset!(app, "aerial_view_lut.wgsl"); + embedded_asset!(app, "render_sky.wgsl"); app.register_type::() .register_type::() @@ -246,7 +193,7 @@ impl Plugin for AtmospherePlugin { /// from the planet's surface, ozone only exists in a band centered at a fairly /// high altitude. #[derive(Clone, Component, Reflect, ShaderType)] -#[require(AtmosphereSettings)] +#[require(AtmosphereSettings, Hdr)] #[reflect(Clone, Default)] pub struct Atmosphere { /// Radius of the planet @@ -362,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()) } } @@ -458,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 b872916619..3f4da25fc0 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -1,6 +1,6 @@ -use bevy_core_pipeline::{ - core_3d::Camera3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state, -}; +use crate::{GpuLights, LightMeta}; +use bevy_asset::{load_embedded_asset, Handle}; +use bevy_core_pipeline::{core_3d::Camera3d, FullscreenShader}; use bevy_ecs::{ component::Component, entity::Entity, @@ -9,6 +9,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut}, world::{FromWorld, World}, }; +use bevy_image::ToExtents; use bevy_math::{Mat4, Vec3}; use bevy_render::{ camera::Camera, @@ -18,10 +19,9 @@ use bevy_render::{ texture::{CachedTexture, TextureCache}, view::{ExtractedView, Msaa, ViewDepthTexture, ViewUniform, ViewUniforms}, }; +use bevy_utils::default; -use crate::{GpuLights, LightMeta}; - -use super::{shaders, Atmosphere, AtmosphereSettings}; +use super::{Atmosphere, AtmosphereSettings}; #[derive(Resource)] pub(crate) struct AtmosphereBindGroupLayouts { @@ -35,6 +35,8 @@ pub(crate) struct AtmosphereBindGroupLayouts { pub(crate) struct RenderSkyBindGroupLayouts { pub render_sky: BindGroupLayout, pub render_sky_msaa: BindGroupLayout, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, } impl FromWorld for AtmosphereBindGroupLayouts { @@ -203,6 +205,8 @@ impl FromWorld for RenderSkyBindGroupLayouts { Self { render_sky, render_sky_msaa, + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "render_sky.wgsl"), } } } @@ -272,42 +276,30 @@ impl FromWorld for AtmosphereLutPipelines { let transmittance_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("transmittance_lut_pipeline".into()), layout: vec![layouts.transmittance_lut.clone()], - push_constant_ranges: vec![], - shader: shaders::TRANSMITTANCE_LUT, - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + shader: load_embedded_asset!(world, "transmittance_lut.wgsl"), + ..default() }); let multiscattering_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("multi_scattering_lut_pipeline".into()), layout: vec![layouts.multiscattering_lut.clone()], - push_constant_ranges: vec![], - shader: shaders::MULTISCATTERING_LUT, - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + shader: load_embedded_asset!(world, "multiscattering_lut.wgsl"), + ..default() }); let sky_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("sky_view_lut_pipeline".into()), layout: vec![layouts.sky_view_lut.clone()], - push_constant_ranges: vec![], - shader: shaders::SKY_VIEW_LUT, - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + shader: load_embedded_asset!(world, "sky_view_lut.wgsl"), + ..default() }); let aerial_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("aerial_view_lut_pipeline".into()), layout: vec![layouts.aerial_view_lut.clone()], - push_constant_ranges: vec![], - shader: shaders::AERIAL_VIEW_LUT, - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + shader: load_embedded_asset!(world, "aerial_view_lut.wgsl"), + ..default() }); Self { @@ -325,7 +317,6 @@ pub(crate) struct RenderSkyPipelineId(pub CachedRenderPipelineId); #[derive(Copy, Clone, Hash, PartialEq, Eq)] pub(crate) struct RenderSkyPipelineKey { pub msaa_samples: u32, - pub hdr: bool, pub dual_source_blending: bool, } @@ -338,9 +329,6 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { if key.msaa_samples > 1 { shader_defs.push("MULTISAMPLED".into()); } - if key.hdr { - shader_defs.push("TONEMAP_IN_SHADER".into()); - } if key.dual_source_blending { shader_defs.push("DUAL_SOURCE_BLENDING".into()); } @@ -358,20 +346,10 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { } else { self.render_sky_msaa.clone() }], - push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState { - count: key.msaa_samples, - mask: !0, - alpha_to_coverage_enabled: false, - }, - zero_initialize_workgroup_memory: false, + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: shaders::RENDER_SKY.clone(), + shader: self.fragment_shader.clone(), shader_defs, - entry_point: "main".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::Rgba16Float, blend: Some(BlendState { @@ -388,26 +366,31 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { }), write_mask: ColorWrites::ALL, })], + ..default() }), + multisample: MultisampleState { + count: key.msaa_samples, + ..default() + }, + ..default() } } } pub(super) fn queue_render_sky_pipelines( - views: Query<(Entity, &Camera, &Msaa), With>, + views: Query<(Entity, &Msaa), (With, With)>, pipeline_cache: Res, layouts: Res, mut specializer: ResMut>, render_device: Res, mut commands: Commands, ) { - for (entity, camera, msaa) in &views { + for (entity, msaa) in &views { let id = specializer.specialize( &pipeline_cache, &layouts, RenderSkyPipelineKey { msaa_samples: msaa.samples(), - hdr: camera.hdr, dual_source_blending: render_device .features() .contains(WgpuFeatures::DUAL_SOURCE_BLENDING), @@ -436,11 +419,7 @@ pub(super) fn prepare_atmosphere_textures( &render_device, TextureDescriptor { label: Some("transmittance_lut"), - size: Extent3d { - width: lut_settings.transmittance_lut_size.x, - height: lut_settings.transmittance_lut_size.y, - depth_or_array_layers: 1, - }, + size: lut_settings.transmittance_lut_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -454,11 +433,7 @@ pub(super) fn prepare_atmosphere_textures( &render_device, TextureDescriptor { label: Some("multiscattering_lut"), - size: Extent3d { - width: lut_settings.multiscattering_lut_size.x, - height: lut_settings.multiscattering_lut_size.y, - depth_or_array_layers: 1, - }, + size: lut_settings.multiscattering_lut_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -472,11 +447,7 @@ pub(super) fn prepare_atmosphere_textures( &render_device, TextureDescriptor { label: Some("sky_view_lut"), - size: Extent3d { - width: lut_settings.sky_view_lut_size.x, - height: lut_settings.sky_view_lut_size.y, - depth_or_array_layers: 1, - }, + size: lut_settings.sky_view_lut_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -490,11 +461,7 @@ pub(super) fn prepare_atmosphere_textures( &render_device, TextureDescriptor { label: Some("aerial_view_lut"), - size: Extent3d { - width: lut_settings.aerial_view_lut_size.x, - height: lut_settings.aerial_view_lut_size.y, - depth_or_array_layers: lut_settings.aerial_view_lut_size.z, - }, + size: lut_settings.aerial_view_lut_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D3, @@ -562,7 +529,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/mod.rs b/crates/bevy_pbr/src/cluster.rs similarity index 64% rename from crates/bevy_pbr/src/cluster/mod.rs rename to crates/bevy_pbr/src/cluster.rs index 3113333be3..9c47859deb 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster.rs @@ -1,40 +1,21 @@ -//! Spatial clustering of objects, currently just point and spot lights. - use core::num::NonZero; -use bevy_core_pipeline::core_3d::Camera3d; -use bevy_ecs::{ - component::Component, - entity::{Entity, EntityHashMap}, - query::{With, Without}, - reflect::ReflectComponent, - resource::Resource, - system::{Commands, Query, Res}, - world::{FromWorld, World}, -}; -use bevy_math::{uvec4, AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4}; -use bevy_platform::collections::HashSet; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_camera::Camera; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_light::cluster::{ClusterableObjectCounts, Clusters, GlobalClusterSettings}; +use bevy_math::{uvec4, UVec3, UVec4, Vec4}; use bevy_render::{ - camera::Camera, render_resource::{ - BindingResource, BufferBindingType, ShaderSize as _, ShaderType, StorageBuffer, - UniformBuffer, + BindingResource, BufferBindingType, ShaderSize, ShaderType, StorageBuffer, UniformBuffer, }, - renderer::{RenderDevice, RenderQueue}, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, sync_world::RenderEntity, Extract, }; use tracing::warn; -pub(crate) use crate::cluster::assign::assign_objects_to_clusters; use crate::MeshPipeline; -pub(crate) mod assign; - -#[cfg(test)] -mod test; - // NOTE: this must be kept in sync with the same constants in // `mesh_view_types.wgsl`. pub const MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS: usize = 204; @@ -54,99 +35,21 @@ const CLUSTER_COUNT_SIZE: u32 = 9; const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - (CLUSTER_COUNT_SIZE * 2))) - 1; const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1; -// Clustered-forward rendering notes -// The main initial reference material used was this rather accessible article: -// http://www.aortiz.me/2018/12/21/CG.html -// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of: -// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/ -// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.) -// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016: -// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf - -/// Configure the far z-plane mode used for the furthest depth slice for clustered forward -/// rendering -#[derive(Debug, Copy, Clone, Reflect)] -#[reflect(Clone)] -pub enum ClusterFarZMode { - /// Calculate the required maximum z-depth based on currently visible - /// clusterable objects. Makes better use of available clusters, speeding - /// up GPU lighting operations at the expense of some CPU time and using - /// more indices in the clusterable object index lists. - MaxClusterableObjectRange, - /// Constant max z-depth - Constant(f32), -} - -/// Configure the depth-slicing strategy for clustered forward rendering -#[derive(Debug, Copy, Clone, Reflect)] -#[reflect(Default, Clone)] -pub struct ClusterZConfig { - /// Far `Z` plane of the first depth slice - pub first_slice_depth: f32, - /// Strategy for how to evaluate the far `Z` plane of the furthest depth slice - pub far_z_mode: ClusterFarZMode, -} - -/// Configuration of the clustering strategy for clustered forward rendering -#[derive(Debug, Copy, Clone, Component, Reflect)] -#[reflect(Component, Debug, Default, Clone)] -pub enum ClusterConfig { - /// Disable cluster calculations for this view - None, - /// One single cluster. Optimal for low-light complexity scenes or scenes where - /// most lights affect the entire scene. - Single, - /// Explicit `X`, `Y` and `Z` counts (may yield non-square `X/Y` clusters depending on the aspect ratio) - XYZ { - dimensions: UVec3, - z_config: ClusterZConfig, - /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding - /// the available cluster-object index limit - dynamic_resizing: bool, - }, - /// Fixed number of `Z` slices, `X` and `Y` calculated to give square clusters - /// with at most total clusters. For top-down games where lights will generally always be within a - /// short depth range, it may be useful to use this configuration with 1 or few `Z` slices. This - /// would reduce the number of lights per cluster by distributing more clusters in screen space - /// `X/Y` which matches how lights are distributed in the scene. - FixedZ { - total: u32, - z_slices: u32, - z_config: ClusterZConfig, - /// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding - /// the available clusterable object index limit - dynamic_resizing: bool, - }, -} - -#[derive(Component, Debug, Default)] -pub struct Clusters { - /// Tile size - pub(crate) tile_size: UVec2, - /// Number of clusters in `X` / `Y` / `Z` in the view frustum - pub(crate) dimensions: UVec3, - /// Distance to the far plane of the first depth slice. The first depth slice is special - /// and explicitly-configured to avoid having unnecessarily many slices close to the camera. - pub(crate) near: f32, - pub(crate) far: f32, - pub(crate) clusterable_objects: Vec, -} - -#[derive(Clone, Component, Debug, Default)] -pub struct VisibleClusterableObjects { - pub(crate) entities: Vec, - counts: ClusterableObjectCounts, -} - -#[derive(Resource, Default)] -pub struct GlobalVisibleClusterableObjects { - pub(crate) entities: HashSet, -} - -#[derive(Resource)] -pub struct GlobalClusterableObjectMeta { - pub gpu_clusterable_objects: GpuClusterableObjects, - pub entity_to_index: EntityHashMap, +pub(crate) fn make_global_cluster_settings(world: &World) -> GlobalClusterSettings { + let device = world.resource::(); + let adapter = world.resource::(); + let clustered_decals_are_usable = + crate::decal::clustered::clustered_decals_are_usable(device, adapter); + let supports_storage_buffers = matches!( + device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT), + BufferBindingType::Storage { .. } + ); + GlobalClusterSettings { + supports_storage_buffers, + clustered_decals_are_usable, + max_uniform_buffer_clusterable_objects: MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, + view_cluster_bindings_max_indices: ViewClusterBindings::MAX_INDICES, + } } #[derive(Copy, Clone, ShaderType, Default, Debug)] @@ -162,8 +65,14 @@ pub struct GpuClusterableObject { pub(crate) spot_light_tan_angle: f32, pub(crate) soft_shadow_size: f32, pub(crate) shadow_map_near_z: f32, - pub(crate) pad_a: f32, - pub(crate) pad_b: f32, + pub(crate) decal_index: u32, + pub(crate) pad: f32, +} + +#[derive(Resource)] +pub struct GlobalClusterableObjectMeta { + pub gpu_clusterable_objects: GpuClusterableObjects, + pub entity_to_index: EntityHashMap, } pub enum GpuClusterableObjects { @@ -191,24 +100,6 @@ pub struct ExtractedClusterConfig { pub(crate) dimensions: UVec3, } -/// Stores the number of each type of clusterable object in a single cluster. -/// -/// Note that `reflection_probes` and `irradiance_volumes` won't be clustered if -/// fewer than 3 SSBOs are available, which usually means on WebGL 2. -#[derive(Clone, Copy, Default, Debug)] -struct ClusterableObjectCounts { - /// The number of point lights in the cluster. - point_lights: u32, - /// The number of spot lights in the cluster. - spot_lights: u32, - /// The number of reflection probes in the cluster. - reflection_probes: u32, - /// The number of irradiance volumes in the cluster. - irradiance_volumes: u32, - /// The number of decals in the cluster. - decals: u32, -} - enum ExtractedClusterableObjectElement { ClusterHeader(ClusterableObjectCounts), ClusterableObjectEntity(Entity), @@ -259,175 +150,6 @@ pub struct ViewClusterBindings { buffers: ViewClusterBuffers, } -impl Default for ClusterZConfig { - fn default() -> Self { - Self { - first_slice_depth: 5.0, - far_z_mode: ClusterFarZMode::MaxClusterableObjectRange, - } - } -} - -impl Default for ClusterConfig { - fn default() -> Self { - // 24 depth slices, square clusters with at most 4096 total clusters - // use max light distance as clusters max `Z`-depth, first slice extends to 5.0 - Self::FixedZ { - total: 4096, - z_slices: 24, - z_config: ClusterZConfig::default(), - dynamic_resizing: true, - } - } -} - -impl ClusterConfig { - fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 { - match &self { - ClusterConfig::None => UVec3::ZERO, - ClusterConfig::Single => UVec3::ONE, - ClusterConfig::XYZ { dimensions, .. } => *dimensions, - ClusterConfig::FixedZ { - total, z_slices, .. - } => { - let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y) - .expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values") - .ratio(); - let mut z_slices = *z_slices; - if *total < z_slices { - warn!("ClusterConfig has more z-slices than total clusters!"); - z_slices = *total; - } - let per_layer = *total as f32 / z_slices as f32; - - let y = f32::sqrt(per_layer / aspect_ratio); - - let mut x = (y * aspect_ratio) as u32; - let mut y = y as u32; - - // check extremes - if x == 0 { - x = 1; - y = per_layer as u32; - } - if y == 0 { - x = per_layer as u32; - y = 1; - } - - UVec3::new(x, y, z_slices) - } - } - } - - fn first_slice_depth(&self) -> f32 { - match self { - ClusterConfig::None | ClusterConfig::Single => 0.0, - ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => { - z_config.first_slice_depth - } - } - } - - fn far_z_mode(&self) -> ClusterFarZMode { - match self { - ClusterConfig::None => ClusterFarZMode::Constant(0.0), - ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange, - ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => { - z_config.far_z_mode - } - } - } - - fn dynamic_resizing(&self) -> bool { - match self { - ClusterConfig::None | ClusterConfig::Single => false, - ClusterConfig::XYZ { - dynamic_resizing, .. - } - | ClusterConfig::FixedZ { - dynamic_resizing, .. - } => *dynamic_resizing, - } - } -} - -impl Clusters { - fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) { - debug_assert!( - requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0 - ); - - let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2()) - .ceil() - .as_uvec2() - .max(UVec2::ONE); - self.tile_size = tile_size; - self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2()) - .ceil() - .as_uvec2() - .extend(requested_dimensions.z) - .max(UVec3::ONE); - - // NOTE: Maximum 4096 clusters due to uniform buffer size constraints - debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096); - } - fn clear(&mut self) { - self.tile_size = UVec2::ONE; - self.dimensions = UVec3::ZERO; - self.near = 0.0; - self.far = 0.0; - self.clusterable_objects.clear(); - } -} - -pub fn add_clusters( - mut commands: Commands, - cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), (Without, With)>, -) { - for (entity, config, camera) in &cameras { - if !camera.is_active { - continue; - } - - let config = config.copied().unwrap_or_default(); - // actual settings here don't matter - they will be overwritten in - // `assign_objects_to_clusters`` - commands - .entity(entity) - .insert((Clusters::default(), config)); - } -} - -impl VisibleClusterableObjects { - #[inline] - pub fn iter(&self) -> impl DoubleEndedIterator { - self.entities.iter() - } - - #[inline] - pub fn len(&self) -> usize { - self.entities.len() - } - - #[inline] - pub fn is_empty(&self) -> bool { - self.entities.is_empty() - } -} - -impl GlobalVisibleClusterableObjects { - #[inline] - pub fn iter(&self) -> impl Iterator { - self.entities.iter() - } - - #[inline] - pub fn contains(&self, entity: Entity) -> bool { - self.entities.contains(&entity) - } -} - impl FromWorld for GlobalClusterableObjectMeta { fn from_world(world: &mut World) -> Self { Self::new( @@ -535,12 +257,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/components.rs b/crates/bevy_pbr/src/components.rs index fca31b3b03..4c451e53f5 100644 --- a/crates/bevy_pbr/src/components.rs +++ b/crates/bevy_pbr/src/components.rs @@ -1,19 +1,12 @@ +pub use bevy_camera::visibility::{ + CascadesVisibleEntities, CubemapVisibleEntities, VisibleMeshEntities, +}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::component::Component; use bevy_ecs::entity::{Entity, EntityHashMap}; use bevy_ecs::reflect::ReflectComponent; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::sync_world::MainEntity; -/// Collection of mesh entities visible for 3D lighting. -/// -/// This component contains all mesh entities visible from the current light view. -/// The collection is updated automatically by [`crate::SimulationLightSystems`]. -#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] -#[reflect(Component, Debug, Default, Clone)] -pub struct VisibleMeshEntities { - #[reflect(ignore, clone)] - pub entities: Vec, -} #[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] #[reflect(Component, Debug, Default, Clone)] @@ -22,31 +15,6 @@ pub struct RenderVisibleMeshEntities { pub entities: Vec<(Entity, MainEntity)>, } -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Debug, Default, Clone)] -pub struct CubemapVisibleEntities { - #[reflect(ignore, clone)] - data: [VisibleMeshEntities; 6], -} - -impl CubemapVisibleEntities { - pub fn get(&self, i: usize) -> &VisibleMeshEntities { - &self.data[i] - } - - pub fn get_mut(&mut self, i: usize) -> &mut VisibleMeshEntities { - &mut self.data[i] - } - - pub fn iter(&self) -> impl DoubleEndedIterator { - self.data.iter() - } - - pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { - self.data.iter_mut() - } -} - #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Debug, Default, Clone)] pub struct RenderCubemapVisibleEntities { @@ -72,14 +40,6 @@ impl RenderCubemapVisibleEntities { } } -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Default, Clone)] -pub struct CascadesVisibleEntities { - /// Map of view entity to the visible entities for each cascade frustum. - #[reflect(ignore, clone)] - pub entities: EntityHashMap>, -} - #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Default, Clone)] pub struct RenderCascadesVisibleEntities { diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index cadcd1b871..7580f1475e 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -17,44 +17,39 @@ use core::{num::NonZero, ops::Deref}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::AssetId; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, entity::{Entity, EntityHashMap}, - prelude::ReflectComponent, query::With, resource::Resource, schedule::IntoScheduleConfigs as _, system::{Query, Res, ResMut}, }; use bevy_image::Image; +pub use bevy_light::cluster::ClusteredDecal; +use bevy_light::{DirectionalLightTexture, PointLightTexture, SpotLightTexture}; use bevy_math::Mat4; use bevy_platform::collections::HashMap; -use bevy_reflect::Reflect; +pub use bevy_render::primitives::CubemapLayout; use bevy_render::{ - extract_component::{ExtractComponent, ExtractComponentPlugin}, + extract_component::ExtractComponentPlugin, + load_shader_library, render_asset::RenderAssets, render_resource::{ binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler, - SamplerBindingType, Shader, ShaderType, TextureSampleType, TextureView, + SamplerBindingType, ShaderType, TextureSampleType, TextureView, }, renderer::{RenderAdapter, RenderDevice, RenderQueue}, sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, - view::{self, ViewVisibility, Visibility, VisibilityClass}, + view::ViewVisibility, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; -use bevy_transform::{components::GlobalTransform, prelude::Transform}; +use bevy_transform::components::GlobalTransform; use bytemuck::{Pod, Zeroable}; -use crate::{ - binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass, -}; - -/// The handle to the `clustered.wgsl` shader. -pub(crate) const CLUSTERED_DECAL_SHADER_HANDLE: Handle = - weak_handle!("87929002-3509-42f1-8279-2d2765dd145c"); +use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta}; /// The maximum number of decals that can be present in a view. /// @@ -69,34 +64,6 @@ pub(crate) const MAX_VIEW_DECALS: usize = 8; /// can still be added to a scene, but they won't project any decals. 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 -/// you may find [`Transform::looking_at`] useful). -/// -/// Clustered decals are the highest-quality types of decals that Bevy supports, -/// but they require bindless textures. This means that they presently can't be -/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used -/// with forward or deferred rendering and don't require a prepass. -#[derive(Component, Debug, Clone, Reflect, ExtractComponent)] -#[reflect(Component, Debug, Clone)] -#[require(Transform, Visibility, VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] -pub struct ClusteredDecal { - /// The image that the clustered decal projects. - /// - /// This must be a 2D image. If it has an alpha channel, it'll be alpha - /// blended with the underlying surface and/or other decals. All decal - /// images in the scene must use the same sampler. - pub image: Handle, - - /// An application-specific tag you can use for any purpose you want. - /// - /// See the `clustered_decals` example for an example of use. - pub tag: u32, -} - /// Stores information about all the clustered decals in the scene. #[derive(Resource, Default)] pub struct RenderClusteredDecals { @@ -124,6 +91,29 @@ impl RenderClusteredDecals { self.decals.clear(); self.entity_to_decal_index.clear(); } + + pub fn insert_decal( + &mut self, + entity: Entity, + image: &AssetId, + local_from_world: Mat4, + tag: u32, + ) { + let image_index = self.get_or_insert_image(image); + let decal_index = self.decals.len(); + self.decals.push(RenderClusteredDecal { + local_from_world, + image_index, + tag, + pad_a: 0, + pad_b: 0, + }); + self.entity_to_decal_index.insert(entity, decal_index); + } + + pub fn get(&self, entity: Entity) -> Option { + self.entity_to_decal_index.get(&entity).copied() + } } /// The per-view bind group entries pertaining to decals. @@ -152,12 +142,7 @@ impl Default for DecalsBuffer { impl Plugin for ClusteredDecalPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - CLUSTERED_DECAL_SHADER_HANDLE, - "clustered.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "clustered.wgsl"); app.add_plugins(ExtractComponentPlugin::::default()) .register_type::(); @@ -212,6 +197,30 @@ pub fn extract_decals( &ViewVisibility, )>, >, + spot_light_textures: Extract< + Query<( + RenderEntity, + &SpotLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + point_light_textures: Extract< + Query<( + RenderEntity, + &PointLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + directional_light_textures: Extract< + Query<( + RenderEntity, + &DirectionalLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, mut render_decals: ResMut, ) { // Clear out the `RenderDecals` in preparation for a new frame. @@ -224,22 +233,54 @@ pub fn extract_decals( continue; } - // Insert or add the image. - let image_index = render_decals.get_or_insert_image(&clustered_decal.image.id()); + render_decals.insert_decal( + decal_entity, + &clustered_decal.image.id(), + global_transform.affine().inverse().into(), + clustered_decal.tag, + ); + } - // Record the decal. - let decal_index = render_decals.decals.len(); - render_decals - .entity_to_decal_index - .insert(decal_entity, decal_index); + for (decal_entity, texture, global_transform, view_visibility) in &spot_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } - render_decals.decals.push(RenderClusteredDecal { - local_from_world: global_transform.affine().inverse().into(), - image_index, - tag: clustered_decal.tag, - pad_a: 0, - pad_b: 0, - }); + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + 0, + ); + } + + for (decal_entity, texture, global_transform, view_visibility) in &point_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + texture.cubemap_layout as u32, + ); + } + + for (decal_entity, texture, global_transform, view_visibility) in &directional_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + if texture.tiled { 1 } else { 0 }, + ); } } @@ -385,4 +426,5 @@ pub fn clustered_decals_are_usable( // Re-enable this when `wgpu` has first-class bindless. binding_arrays_are_usable(render_device, render_adapter) && cfg!(not(any(target_os = "macos", target_os = "ios"))) + && cfg!(feature = "pbr_clustered_decals") } diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index 2445c3e723..49767557e1 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -3,10 +3,11 @@ use crate::{ MaterialPlugin, StandardMaterial, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Asset, Assets, Handle}; +use bevy_asset::{uuid_handle, Asset, Assets, Handle}; use bevy_ecs::component::Component; use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3}; use bevy_reflect::{Reflect, TypePath}; +use bevy_render::load_shader_library; use bevy_render::render_asset::RenderAssets; use bevy_render::render_resource::{AsBindGroupShaderType, ShaderType}; use bevy_render::texture::GpuImage; @@ -14,28 +15,20 @@ use bevy_render::{ alpha::AlphaMode, mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable}, render_resource::{ - AsBindGroup, CompareFunction, RenderPipelineDescriptor, Shader, - SpecializedMeshPipelineError, + AsBindGroup, CompareFunction, RenderPipelineDescriptor, SpecializedMeshPipelineError, }, RenderDebugFlags, }; const FORWARD_DECAL_MESH_HANDLE: Handle = - weak_handle!("afa817f9-1869-4e0c-ac0d-d8cd1552d38a"); -const FORWARD_DECAL_SHADER_HANDLE: Handle = - weak_handle!("f8dfbef4-d88b-42ae-9af4-d9661e9f1648"); + uuid_handle!("afa817f9-1869-4e0c-ac0d-d8cd1552d38a"); /// Plugin to render [`ForwardDecal`]s. pub struct ForwardDecalPlugin; impl Plugin for ForwardDecalPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - FORWARD_DECAL_SHADER_HANDLE, - "forward_decal.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "forward_decal.wgsl"); app.register_type::(); @@ -139,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..96569b2861 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -1,14 +1,11 @@ use crate::{ - graph::NodePbr, irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight, - MeshPipeline, MeshViewBindGroup, RenderViewLightProbes, ScreenSpaceAmbientOcclusion, - ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset, ViewLightProbesUniformOffset, + graph::NodePbr, irradiance_volume::IrradianceVolume, MeshPipeline, MeshViewBindGroup, + RenderViewLightProbes, ScreenSpaceAmbientOcclusion, ScreenSpaceReflectionsUniform, + ViewEnvironmentMapUniformOffset, ViewLightProbesUniformOffset, ViewScreenSpaceReflectionsUniformOffset, TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, }; -use crate::{ - DistanceFog, MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset, - ViewLightsUniformOffset, -}; +use crate::{DistanceFog, MeshPipelineKey, ViewFogUniformOffset, ViewLightsUniformOffset}; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ @@ -21,16 +18,18 @@ use bevy_core_pipeline::{ }; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_image::BevyDefault as _; +use bevy_light::{EnvironmentMapLight, ShadowFilteringMethod}; use bevy_render::{ extract_component::{ ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderContext, RenderDevice}, view::{ExtractedView, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; pub struct DeferredPbrLightingPlugin; @@ -184,9 +183,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 +207,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 +217,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 +228,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,22 +346,22 @@ 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(), shader_defs: shader_defs.clone(), - entry_point: "vertex".into(), - buffers: Vec::new(), + ..default() }, fragment: Some(FragmentState { shader: self.deferred_lighting_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR @@ -371,8 +371,8 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, depth_write_enabled: false, @@ -389,9 +389,7 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { clamp: 0.0, }, }), - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -408,7 +406,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 +447,7 @@ pub fn prepare_deferred_lighting_pipelines( ), Has>, Has>, + Has, )>, ) { for ( @@ -461,12 +460,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 +552,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/deferred/pbr_deferred_types.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl index ef39307b49..fb4def94ce 100644 --- a/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl @@ -6,9 +6,9 @@ } // Maximum of 8 bits available -const DEFERRED_FLAGS_UNLIT_BIT: u32 = 1u; -const DEFERRED_FLAGS_FOG_ENABLED_BIT: u32 = 2u; -const DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 4u; +const DEFERRED_FLAGS_UNLIT_BIT: u32 = 1u << 0u; +const DEFERRED_FLAGS_FOG_ENABLED_BIT: u32 = 1u << 1u; +const DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u << 2u; fn deferred_flags_from_mesh_material_flags(mesh_flags: u32, mat_flags: u32) -> u32 { var flags = 0u; diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index e01dd0ff14..165debee6f 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -1,6 +1,6 @@ use alloc::borrow::Cow; -use bevy_asset::{Asset, Handle}; +use bevy_asset::Asset; use bevy_ecs::system::SystemParamItem; use bevy_platform::{collections::HashSet, hash::FixedHasher}; use bevy_reflect::{impl_type_path, Reflect}; @@ -9,8 +9,8 @@ use bevy_render::{ mesh::MeshVertexBufferLayoutRef, render_resource::{ AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor, - BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader, - ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup, + BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, ShaderRef, + SpecializedMeshPipelineError, UnpreparedBindGroup, }, renderer::RenderDevice, }; @@ -19,10 +19,6 @@ use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshP pub struct MaterialExtensionPipeline { pub mesh_pipeline: MeshPipeline, - pub material_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, - pub bindless: bool, } pub struct MaterialExtensionKey { @@ -150,12 +146,19 @@ where } } +#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, PartialEq, Eq, Hash)] +#[repr(C, packed)] +pub struct MaterialExtensionBindGroupData { + pub base: B, + pub extension: E, +} + // We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]` // causes the `TypePath` derive to not generate an implementation. impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial); impl AsBindGroup for ExtendedMaterial { - type Data = (::Data, ::Data); + type Data = MaterialExtensionBindGroupData; type Param = (::Param, ::Param); fn bindless_slot_count() -> Option { @@ -179,20 +182,24 @@ impl AsBindGroup for ExtendedMaterial { } } + fn bind_group_data(&self) -> Self::Data { + MaterialExtensionBindGroupData { + base: self.base.bind_group_data(), + extension: self.extension.bind_group_data(), + } + } + fn unprepared_bind_group( &self, layout: &BindGroupLayout, render_device: &RenderDevice, (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, mut force_non_bindless: bool, - ) -> Result, AsBindGroupError> { + ) -> Result { force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); // add together the bindings of the base material and the user material - let UnpreparedBindGroup { - mut bindings, - data: base_data, - } = B::unprepared_bind_group( + let UnpreparedBindGroup { mut bindings } = B::unprepared_bind_group( &self.base, layout, render_device, @@ -209,10 +216,7 @@ impl AsBindGroup for ExtendedMaterial { bindings.extend(extended_bindgroup.bindings.0); - Ok(UnpreparedBindGroup { - bindings, - data: (base_data, extended_bindgroup.data), - }) + Ok(UnpreparedBindGroup { bindings }) } fn bind_group_layout_entries( @@ -373,57 +377,28 @@ impl Material for ExtendedMaterial { } fn specialize( - pipeline: &MaterialPipeline, + pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { // Call the base material's specialize function - let MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - .. - } = pipeline.clone(); - let base_pipeline = MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - marker: Default::default(), - }; let base_key = MaterialPipelineKey:: { mesh_key: key.mesh_key, - bind_group_data: key.bind_group_data.0, + bind_group_data: key.bind_group_data.base, }; - B::specialize(&base_pipeline, descriptor, layout, base_key)?; + B::specialize(pipeline, descriptor, layout, base_key)?; // Call the extended material's specialize function afterwards - let MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - .. - } = pipeline.clone(); - E::specialize( &MaterialExtensionPipeline { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, + mesh_pipeline: pipeline.mesh_pipeline.clone(), }, descriptor, layout, MaterialExtensionKey { mesh_key: key.mesh_key, - bind_group_data: key.bind_group_data.1, + bind_group_data: key.bind_group_data.extension, }, ) } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 12785f3e78..45aa6297d2 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -2,8 +2,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] extern crate alloc; @@ -31,7 +31,6 @@ pub mod decal; pub mod deferred; mod extended_material; mod fog; -mod light; mod light_probe; mod lightmap; mod material; @@ -48,12 +47,19 @@ mod volumetric_fog; use bevy_color::{Color, LinearRgba}; pub use atmosphere::*; +use bevy_light::SimulationLightSystems; +pub use bevy_light::{ + light_consts, AmbientLight, CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades, + ClusteredDecal, DirectionalLight, DirectionalLightShadowMap, DirectionalLightTexture, + FogVolume, IrradianceVolume, LightPlugin, LightProbe, NotShadowCaster, NotShadowReceiver, + PointLight, PointLightShadowMap, PointLightTexture, ShadowFilteringMethod, SpotLight, + SpotLightTexture, TransmittedShadowReceiver, VolumetricFog, VolumetricLight, +}; pub use cluster::*; pub use components::*; pub use decal::clustered::ClusteredDecalPlugin; pub use extended_material::*; pub use fog::*; -pub use light::*; pub use light_probe::*; pub use lightmap::*; pub use material::*; @@ -65,7 +71,7 @@ pub use prepass::*; pub use render::*; pub use ssao::*; pub use ssr::*; -pub use volumetric_fog::{FogVolume, VolumetricFog, VolumetricFogPlugin, VolumetricLight}; +pub use volumetric_fog::VolumetricFogPlugin; /// The PBR prelude. /// @@ -74,14 +80,17 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ fog::{DistanceFog, FogFalloff}, - light::{light_consts, AmbientLight, DirectionalLight, PointLight, SpotLight}, - light_probe::{environment_map::EnvironmentMapLight, LightProbe}, material::{Material, MaterialPlugin}, mesh_material::MeshMaterial3d, parallax::ParallaxMappingMethod, pbr_material::StandardMaterial, ssao::ScreenSpaceAmbientOcclusionPlugin, }; + #[doc(hidden)] + pub use bevy_light::{ + light_consts, AmbientLight, DirectionalLight, EnvironmentMapLight, LightProbe, PointLight, + SpotLight, + }; } pub mod graph { @@ -124,36 +133,30 @@ pub mod graph { use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetPath, Assets, Handle}; +use bevy_asset::{AssetApp, AssetPath, Assets, Handle}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_render::{ alpha::AlphaMode, - camera::{sort_cameras, CameraUpdateSystems, Projection}, + camera::{sort_cameras, Projection}, extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, load_shader_library, render_graph::RenderGraph, - render_resource::{Shader, ShaderRef}, + render_resource::ShaderRef, sync_component::SyncComponentPlugin, - view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, }; -use bevy_transform::TransformSystems; - use std::path::PathBuf; fn shader_ref(path: PathBuf) -> ShaderRef { ShaderRef::Path(AssetPath::from_path_buf(path).with_source("embedded")) } -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 { @@ -205,33 +208,9 @@ impl Plugin for PbrPlugin { load_shader_library!(app, "render/view_transformations.wgsl"); // Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors. - load_internal_asset!( - app, - MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE, - "meshlet/dummy_visibility_buffer_resolve.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl"); app.register_asset_reflect::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() .register_type::() .init_resource::() .add_plugins(( @@ -239,6 +218,9 @@ impl Plugin for PbrPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, debug_flags: self.debug_flags, }, + MaterialsPlugin { + debug_flags: self.debug_flags, + }, MaterialPlugin:: { prepass_enabled: self.prepass_enabled, debug_flags: self.debug_flags, @@ -251,7 +233,7 @@ impl Plugin for PbrPlugin { ExtractComponentPlugin::::default(), LightmapPlugin, LightProbePlugin, - PbrProjectionPlugin, + LightPlugin, GpuMeshPreprocessPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, }, @@ -274,64 +256,6 @@ impl Plugin for PbrPlugin { SimulationLightSystems::AssignLightsToClusters, ) .chain(), - ) - .configure_sets( - PostUpdate, - SimulationLightSystems::UpdateDirectionalLightCascades - .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades), - ) - .configure_sets( - PostUpdate, - SimulationLightSystems::CheckLightVisibility - .ambiguous_with(SimulationLightSystems::CheckLightVisibility), - ) - .add_systems( - PostUpdate, - ( - add_clusters - .in_set(SimulationLightSystems::AddClusters) - .after(CameraUpdateSystems), - assign_objects_to_clusters - .in_set(SimulationLightSystems::AssignLightsToClusters) - .after(TransformSystems::Propagate) - .after(VisibilitySystems::CheckVisibility) - .after(CameraUpdateSystems), - clear_directional_light_cascades - .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(TransformSystems::Propagate) - .after(CameraUpdateSystems), - update_directional_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - // This must run after CheckVisibility because it relies on `ViewVisibility` - .after(VisibilitySystems::CheckVisibility) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::UpdateDirectionalLightCascades) - // We assume that no entity will be both a directional light and a spot light, - // so these systems will run independently of one another. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(update_spot_light_frusta), - update_point_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::AssignLightsToClusters), - update_spot_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::AssignLightsToClusters), - ( - check_dir_light_mesh_visibility, - check_point_light_mesh_visibility, - ) - .in_set(SimulationLightSystems::CheckLightVisibility) - .after(VisibilitySystems::CalculateBounds) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::UpdateLightFrusta) - // NOTE: This MUST be scheduled AFTER the core renderer visibility check - // because that resets entity `ViewVisibility` for the first view - // which would override any results from this otherwise - .after(VisibilitySystems::CheckVisibility) - .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible), - ), ); if self.add_default_deferred_lighting_plugin { @@ -404,19 +328,8 @@ impl Plugin for PbrPlugin { .init_resource::() .init_resource::() .init_resource::(); - } -} -/// Camera projection PBR functionality. -#[derive(Default)] -pub struct PbrProjectionPlugin; -impl Plugin for PbrProjectionPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - PostUpdate, - build_directional_light_cascades - .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(clear_directional_light_cascades), - ); + let global_cluster_settings = make_global_cluster_settings(render_app.world()); + app.insert_resource(global_cluster_settings); } } diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index 52ccaef432..e6dfebd903 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -44,20 +44,17 @@ //! //! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments -use bevy_asset::{weak_handle, AssetId, Handle}; -use bevy_ecs::{ - component::Component, query::QueryItem, reflect::ReflectComponent, system::lifetimeless::Read, -}; +use bevy_asset::AssetId; +use bevy_ecs::{query::QueryItem, system::lifetimeless::Read}; use bevy_image::Image; -use bevy_math::Quat; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_light::EnvironmentMapLight; use bevy_render::{ extract_instances::ExtractInstance, render_asset::RenderAssets, render_resource::{ binding_types::{self, uniform_buffer}, - BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, ShaderStages, - TextureSampleType, TextureView, + BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, ShaderStages, TextureSampleType, + TextureView, }, renderer::{RenderAdapter, RenderDevice}, texture::{FallbackImage, GpuImage}, @@ -72,60 +69,6 @@ use crate::{ use super::{LightProbeComponent, RenderViewLightProbes}; -/// A handle to the environment map helper shader. -pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle = - weak_handle!("d38c4ec4-e84c-468f-b485-bf44745db937"); - -/// A pair of cubemap textures that represent the surroundings of a specific -/// area in space. -/// -/// See [`crate::environment_map`] for detailed information. -#[derive(Clone, Component, Reflect)] -#[reflect(Component, Default, Clone)] -pub struct EnvironmentMapLight { - /// The blurry image that represents diffuse radiance surrounding a region. - pub diffuse_map: Handle, - - /// The typically-sharper, mipmapped image that represents specular radiance - /// surrounding a region. - pub specular_map: Handle, - - /// Scale factor applied to the diffuse and specular light generated by this component. - /// - /// After applying this multiplier, the resulting values should - /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre). - /// - /// See also . - pub intensity: f32, - - /// World space rotation applied to the environment light cubemaps. - /// This is useful for users who require a different axis, such as the Z-axis, to serve - /// as the vertical axis. - pub rotation: Quat, - - /// Whether the light from this environment map contributes diffuse lighting - /// to meshes with lightmaps. - /// - /// Set this to false if your lightmap baking tool bakes the diffuse light - /// from this environment light into the lightmaps in order to avoid - /// counting the radiance from this environment map twice. - /// - /// By default, this is set to true. - pub affects_lightmapped_mesh_diffuse: bool, -} - -impl Default for EnvironmentMapLight { - fn default() -> Self { - EnvironmentMapLight { - diffuse_map: Handle::default(), - specular_map: Handle::default(), - intensity: 0.0, - rotation: Quat::IDENTITY, - affects_lightmapped_mesh_diffuse: true, - } - } -} - /// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles. /// /// This is for use in the render app. @@ -196,7 +139,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 05dd51c379..95ed8dcbd9 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 @@ -132,22 +133,20 @@ //! //! [Why ambient cubes?]: #why-ambient-cubes -use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_image::Image; +pub use bevy_light::IrradianceVolume; use bevy_render::{ render_asset::RenderAssets, render_resource::{ - binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, - TextureSampleType, TextureView, + binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, TextureSampleType, + TextureView, }, renderer::{RenderAdapter, RenderDevice}, texture::{FallbackImage, GpuImage}, }; -use bevy_utils::default; use core::{num::NonZero, ops::Deref}; -use bevy_asset::{weak_handle, AssetId, Handle}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_asset::AssetId; use crate::{ add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes, @@ -156,57 +155,11 @@ use crate::{ use super::LightProbeComponent; -pub const IRRADIANCE_VOLUME_SHADER_HANDLE: Handle = - weak_handle!("7fc7dcd8-3f90-4124-b093-be0e53e08205"); - /// 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 /// (see issue #11885). pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "wasm32")); -/// The component that defines an irradiance volume. -/// -/// See [`crate::irradiance_volume`] for detailed information. -#[derive(Clone, Reflect, Component, Debug)] -#[reflect(Component, Default, Debug, Clone)] -pub struct IrradianceVolume { - /// The 3D texture that represents the ambient cubes, encoded in the format - /// described in [`crate::irradiance_volume`]. - pub voxels: Handle, - - /// Scale factor applied to the diffuse and specular light generated by this component. - /// - /// After applying this multiplier, the resulting values should - /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre). - /// - /// See also . - pub intensity: f32, - - /// Whether the light from this irradiance volume has an effect on meshes - /// with lightmaps. - /// - /// Set this to false if your lightmap baking tool bakes the light from this - /// irradiance volume into the lightmaps in order to avoid counting the - /// irradiance twice. Frequently, applications use irradiance volumes as a - /// lower-quality alternative to lightmaps for capturing indirect - /// illumination on dynamic objects, and such applications will want to set - /// this value to false. - /// - /// By default, this is set to true. - pub affects_lightmapped_meshes: bool, -} - -impl Default for IrradianceVolume { - #[inline] - fn default() -> Self { - IrradianceVolume { - voxels: default(), - intensity: 0.0, - affects_lightmapped_meshes: true, - } - } -} - /// All the bind group entries necessary for PBR shaders to access the /// irradiance volumes exposed to a view. pub(crate) enum RenderViewIrradianceVolumeBindGroupEntries<'a> { diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index d7323a1e3c..bce844bb21 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -1,32 +1,32 @@ //! Light probes for baked global illumination. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::AssetId; use bevy_core_pipeline::core_3d::Camera3d; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, query::With, - reflect::ReflectComponent, resource::Resource, schedule::IntoScheduleConfigs, system::{Commands, Local, Query, Res, ResMut}, }; use bevy_image::Image; +use bevy_light::{EnvironmentMapLight, LightProbe}; use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4}; use bevy_platform::collections::HashMap; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_instances::ExtractInstancesPlugin, + load_shader_library, primitives::{Aabb, Frustum}, render_asset::RenderAssets, - render_resource::{DynamicUniformBuffer, Sampler, Shader, ShaderType, TextureView}, + render_resource::{DynamicUniformBuffer, Sampler, ShaderType, TextureView}, renderer::{RenderAdapter, RenderDevice, RenderQueue}, settings::WgpuFeatures, sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, - view::{ExtractedView, Visibility}, + view::ExtractedView, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::{components::Transform, prelude::GlobalTransform}; @@ -34,18 +34,10 @@ use tracing::error; use core::{hash::Hash, ops::Deref}; -use crate::{ - irradiance_volume::IRRADIANCE_VOLUME_SHADER_HANDLE, - light_probe::environment_map::{ - EnvironmentMapIds, EnvironmentMapLight, ENVIRONMENT_MAP_SHADER_HANDLE, - }, -}; +use crate::light_probe::environment_map::EnvironmentMapIds; use self::irradiance_volume::IrradianceVolume; -pub const LIGHT_PROBE_SHADER_HANDLE: Handle = - weak_handle!("e80a2ae6-1c5a-4d9a-a852-d66ff0e6bf7f"); - pub mod environment_map; pub mod irradiance_volume; @@ -66,50 +58,6 @@ const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16; /// cubemaps applied to all objects that a view renders. pub struct LightProbePlugin; -/// A marker component for a light probe, which is a cuboid region that provides -/// global illumination to all fragments inside it. -/// -/// Note that a light probe will have no effect unless the entity contains some -/// kind of illumination, which can either be an [`EnvironmentMapLight`] or an -/// [`IrradianceVolume`]. -/// -/// The light probe range is conceptually a unit cube (1×1×1) centered on the -/// origin. The [`Transform`] applied to this entity can scale, rotate, or translate -/// that cube so that it contains all fragments that should take this light probe into account. -/// -/// When multiple sources of indirect illumination can be applied to a fragment, -/// the highest-quality one is chosen. Diffuse and specular illumination are -/// considered separately, so, for example, Bevy may decide to sample the -/// diffuse illumination from an irradiance volume and the specular illumination -/// from a reflection probe. From highest priority to lowest priority, the -/// ranking is as follows: -/// -/// | Rank | Diffuse | Specular | -/// | ---- | -------------------- | -------------------- | -/// | 1 | Lightmap | Lightmap | -/// | 2 | Irradiance volume | Reflection probe | -/// | 3 | Reflection probe | View environment map | -/// | 4 | View environment map | | -/// -/// Note that ambient light is always added to the diffuse component and does -/// not participate in the ranking. That is, ambient light is applied in -/// addition to, not instead of, the light sources above. -/// -/// A terminology note: Unfortunately, there is little agreement across game and -/// graphics engines as to what to call the various techniques that Bevy groups -/// under the term *light probe*. In Bevy, a *light probe* is the generic term -/// that encompasses both *reflection probes* and *irradiance volumes*. In -/// object-oriented terms, *light probe* is the superclass, and *reflection -/// probe* and *irradiance volume* are subclasses. In other engines, you may see -/// the term *light probe* refer to an irradiance volume with a single voxel, or -/// perhaps some other technique, while in Bevy *light probe* refers not to a -/// specific technique but rather to a class of techniques. Developers familiar -/// with other engines should be aware of this terminology difference. -#[derive(Component, Debug, Clone, Copy, Default, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -#[require(Transform, Visibility)] -pub struct LightProbe; - /// A GPU type that stores information about a light probe. #[derive(Clone, Copy, ShaderType, Default)] struct RenderLightProbe { @@ -309,14 +257,6 @@ pub trait LightProbeComponent: Send + Sync + Component + Sized { ) -> RenderViewLightProbes; } -impl LightProbe { - /// Creates a new light probe component. - #[inline] - pub fn new() -> Self { - Self - } -} - /// The uniform struct extracted from [`EnvironmentMapLight`]. /// Will be available for use in the Environment Map shader. #[derive(Component, ShaderType, Clone)] @@ -344,28 +284,11 @@ pub struct ViewEnvironmentMapUniformOffset(u32); impl Plugin for LightProbePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - LIGHT_PROBE_SHADER_HANDLE, - "light_probe.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - ENVIRONMENT_MAP_SHADER_HANDLE, - "environment_map.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - IRRADIANCE_VOLUME_SHADER_HANDLE, - "irradiance_volume.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "light_probe.wgsl"); + load_shader_library!(app, "environment_map.wgsl"); + load_shader_library!(app, "irradiance_volume.wgsl"); - app.register_type::() - .register_type::() - .register_type::(); + app.add_plugins(ExtractInstancesPlugin::::new()); } fn finish(&self, app: &mut App) { @@ -374,7 +297,6 @@ impl Plugin for LightProbePlugin { }; render_app - .add_plugins(ExtractInstancesPlugin::::new()) .init_resource::() .init_resource::() .add_systems(ExtractSchedule, gather_environment_map_uniform) @@ -400,7 +322,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 { @@ -617,7 +539,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/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 5ddb1d6c72..567bbce674 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -32,14 +32,14 @@ //! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::{AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + lifecycle::RemovedComponents, query::{Changed, Or}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs, system::{Query, Res, ResMut}, @@ -50,8 +50,9 @@ use bevy_math::{uvec2, vec4, Rect, UVec2}; use bevy_platform::collections::HashSet; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ + load_shader_library, render_asset::RenderAssets, - render_resource::{Sampler, Shader, TextureView, WgpuSampler, WgpuTextureView}, + render_resource::{Sampler, TextureView, WgpuSampler, WgpuTextureView}, renderer::RenderAdapter, sync_world::MainEntity, texture::{FallbackImage, GpuImage}, @@ -66,10 +67,6 @@ use tracing::error; use crate::{binding_arrays_are_usable, MeshExtractionSystems}; -/// The ID of the lightmap shader. -pub const LIGHTMAP_SHADER_HANDLE: Handle = - weak_handle!("fc28203f-f258-47f3-973c-ce7d1dd70e59"); - /// The number of lightmaps that we store in a single slab, if bindless textures /// are in use. /// @@ -188,12 +185,7 @@ pub struct LightmapSlotIndex(pub(crate) NonMaxU16); impl Plugin for LightmapPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - LIGHTMAP_SHADER_HANDLE, - "lightmap.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "lightmap.wgsl"); } fn finish(&self, app: &mut App) { diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index af11db1ba6..cc3a69a0ad 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,12 +1,8 @@ use crate::material_bind_groups::{ FallbackBindlessResources, MaterialBindGroupAllocator, MaterialBindingId, }; -#[cfg(feature = "meshlet")] -use crate::meshlet::{ - prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, - InstanceManager, -}; use crate::*; +use alloc::sync::Arc; use bevy_asset::prelude::AssetChanged; use bevy_asset::{Asset, AssetEventSystems, AssetId, AssetServer, UntypedAssetId}; use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; @@ -35,14 +31,18 @@ use bevy_platform::hash::FixedHasher; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::extract_cameras; +use bevy_render::erased_render_asset::{ + ErasedRenderAsset, ErasedRenderAssetPlugin, ErasedRenderAssets, PrepareAssetError, +}; use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed; -use bevy_render::render_asset::prepare_assets; +use bevy_render::render_asset::{prepare_assets, RenderAssets}; use bevy_render::renderer::RenderQueue; +use bevy_render::RenderStartup; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingSupport, extract_resource::ExtractResource, mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, - render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, + prelude::*, render_phase::*, render_resource::*, renderer::RenderDevice, @@ -53,7 +53,9 @@ use bevy_render::{ use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; use bevy_utils::Parallel; +use core::any::TypeId; use core::{hash::Hash, marker::PhantomData}; +use smallvec::SmallVec; use tracing::error; /// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] @@ -120,9 +122,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 @@ -239,7 +241,7 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { )] #[inline] fn specialize( - pipeline: &MaterialPipeline, + pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, @@ -248,6 +250,74 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { } } +#[derive(Default)] +pub struct MaterialsPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Plugin for MaterialsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((PrepassPipelinePlugin, PrepassPlugin::new(self.debug_flags))); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + ( + specialize_material_meshes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(collect_meshes_for_gpu_building) + .after(set_mesh_motion_vector_flags), + queue_material_meshes.in_set(RenderSystems::QueueMeshes), + ), + ) + .add_systems( + Render, + ( + prepare_material_bind_groups, + write_material_bind_group_buffers, + ) + .chain() + .in_set(RenderSystems::PrepareBindGroups), + ) + .add_systems( + Render, + ( + check_views_lights_need_specialization.in_set(RenderSystems::PrepareAssets), + // specialize_shadows also needs to run after prepare_assets::, + // which is fine since ManageViews is after PrepareAssets + specialize_shadows + .in_set(RenderSystems::ManageViews) + .after(prepare_lights), + queue_shadows.in_set(RenderSystems::QueueMeshes), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::(); + } + } +} + /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] /// asset type. pub struct MaterialPlugin { @@ -283,7 +353,7 @@ where app.init_asset::() .register_type::>() .init_resource::>() - .add_plugins((RenderAssetPlugin::>::default(),)) + .add_plugins((ErasedRenderAssetPlugin::>::default(),)) .add_systems( PostUpdate, ( @@ -302,17 +372,15 @@ where } if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + if self.prepass_enabled { + render_app.init_resource::>(); + } + if self.shadows_enabled { + render_app.init_resource::>(); + } + render_app - .init_resource::>() - .init_resource::>() - .init_resource::>() - .init_resource::() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .init_resource::>>() + .add_systems(RenderStartup, setup_render_app::) .add_systems( ExtractSchedule, ( @@ -322,90 +390,27 @@ where .before(late_sweep_material_instances), extract_entities_needs_specialization::.after(extract_cameras), ), - ) - .add_systems( - Render, - ( - specialize_material_meshes:: - .in_set(RenderSystems::PrepareMeshes) - .after(prepare_assets::>) - .after(prepare_assets::) - .after(collect_meshes_for_gpu_building) - .after(set_mesh_motion_vector_flags), - queue_material_meshes:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>), - ), - ) - .add_systems( - Render, - ( - prepare_material_bind_groups::, - write_material_bind_group_buffers::, - ) - .chain() - .in_set(RenderSystems::PrepareBindGroups) - .after(prepare_assets::>), ); - - if self.shadows_enabled { - render_app - .init_resource::() - .init_resource::() - .init_resource::>() - .add_systems( - Render, - ( - check_views_lights_need_specialization - .in_set(RenderSystems::PrepareAssets), - // specialize_shadows:: also needs to run after prepare_assets::>, - // which is fine since ManageViews is after PrepareAssets - specialize_shadows:: - .in_set(RenderSystems::ManageViews) - .after(prepare_lights), - queue_shadows:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>), - ), - ); - } - - #[cfg(feature = "meshlet")] - render_app.add_systems( - Render, - queue_material_meshlet_meshes:: - .in_set(RenderSystems::QueueMeshes) - .run_if(resource_exists::), - ); - - #[cfg(feature = "meshlet")] - render_app.add_systems( - Render, - prepare_material_meshlet_meshes_main_opaque_pass:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - .before(queue_material_meshlet_meshes::) - .run_if(resource_exists::), - ); - } - - if self.shadows_enabled || self.prepass_enabled { - // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin - app.add_plugins(PrepassPipelinePlugin::::default()); - } - - if self.prepass_enabled { - app.add_plugins(PrepassPlugin::::new(self.debug_flags)); } } +} - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::>() - .init_resource::>(); - } - } +fn setup_render_app( + render_device: Res, + mut bind_group_allocators: ResMut, +) { + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new( + &render_device, + M::label(), + material_uses_bindless_resources::(&render_device) + .then(|| M::bindless_descriptor()) + .flatten(), + M::bind_group_layout(&render_device), + M::bindless_slot_count(), + ), + ); } /// A dummy [`AssetId`] that we use as a placeholder whenever a mesh doesn't @@ -422,91 +427,54 @@ pub struct MaterialPipelineKey { pub bind_group_data: M::Data, } -impl Eq for MaterialPipelineKey where M::Data: PartialEq {} - -impl PartialEq for MaterialPipelineKey -where - M::Data: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.mesh_key == other.mesh_key && self.bind_group_data == other.bind_group_data - } -} - -impl Clone for MaterialPipelineKey -where - M::Data: Clone, -{ - fn clone(&self) -> Self { - Self { - mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data.clone(), - } - } -} - -impl Hash for MaterialPipelineKey -where - M::Data: Hash, -{ - fn hash(&self, state: &mut H) { - self.mesh_key.hash(state); - self.bind_group_data.hash(state); - } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ErasedMaterialPipelineKey { + pub mesh_key: MeshPipelineKey, + pub material_key: SmallVec<[u8; 8]>, + pub type_id: TypeId, } /// Render pipeline data for a given [`Material`]. -#[derive(Resource)] -pub struct MaterialPipeline { +#[derive(Resource, Clone)] +pub struct MaterialPipeline { pub mesh_pipeline: MeshPipeline, - pub material_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, - /// Whether this material *actually* uses bindless resources, taking the - /// platform support (or lack thereof) of bindless resources into account. - pub bindless: bool, - pub marker: PhantomData, } -impl Clone for MaterialPipeline { - fn clone(&self) -> Self { - Self { - mesh_pipeline: self.mesh_pipeline.clone(), - material_layout: self.material_layout.clone(), - vertex_shader: self.vertex_shader.clone(), - fragment_shader: self.fragment_shader.clone(), - bindless: self.bindless, - marker: PhantomData, - } - } +pub struct MaterialPipelineSpecializer { + pub(crate) pipeline: MaterialPipeline, + pub(crate) properties: Arc, } -impl SpecializedMeshPipeline for MaterialPipeline -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - type Key = MaterialPipelineKey; +impl SpecializedMeshPipeline for MaterialPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayoutRef, ) -> Result { - let mut descriptor = self.mesh_pipeline.specialize(key.mesh_key, layout)?; - if let Some(vertex_shader) = &self.vertex_shader { + let mut descriptor = self + .pipeline + .mesh_pipeline + .specialize(key.mesh_key, layout)?; + if let Some(vertex_shader) = self.properties.get_shader(MaterialVertexShader) { descriptor.vertex.shader = vertex_shader.clone(); } - if let Some(fragment_shader) = &self.fragment_shader { + if let Some(fragment_shader) = self.properties.get_shader(MaterialFragmentShader) { descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); } - descriptor.layout.insert(2, self.material_layout.clone()); + descriptor + .layout + .insert(3, self.properties.material_layout.as_ref().unwrap().clone()); - M::specialize(self, &mut descriptor, layout, key)?; + if let Some(specialize) = self.properties.specialize { + specialize(&self.pipeline, &mut descriptor, layout, key)?; + } // If bindless mode is on, add a `BINDLESS` define. - if self.bindless { + if self.properties.bindless { descriptor.vertex.shader_defs.push("BINDLESS".into()); if let Some(ref mut fragment) = descriptor.fragment { fragment.shader_defs.push("BINDLESS".into()); @@ -517,45 +485,30 @@ where } } -impl FromWorld for MaterialPipeline { +impl FromWorld for MaterialPipeline { fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - let render_device = world.resource::(); - MaterialPipeline { mesh_pipeline: world.resource::().clone(), - material_layout: M::bind_group_layout(render_device), - vertex_shader: match M::vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - fragment_shader: match M::fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - bindless: material_uses_bindless_resources::(render_device), - marker: PhantomData, } } } -type DrawMaterial = ( +pub type DrawMaterial = ( SetItemPipeline, SetMeshViewBindGroup<0>, - SetMeshBindGroup<1>, - SetMaterialBindGroup, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup<3>, DrawMesh, ); /// Sets the bind group for a given [`Material`] at the configured `I` index. -pub struct SetMaterialBindGroup(PhantomData); -impl RenderCommand

for SetMaterialBindGroup { +pub struct SetMaterialBindGroup; +impl RenderCommand

for SetMaterialBindGroup { type Param = ( - SRes>>, + SRes>, SRes, - SRes>, + SRes, ); type ViewQuery = (); type ItemQuery = (); @@ -574,15 +527,17 @@ impl RenderCommand

for SetMaterial ) -> RenderCommandResult { let materials = materials.into_inner(); let material_instances = material_instances.into_inner(); - let material_bind_group_allocator = material_bind_group_allocator.into_inner(); + let material_bind_group_allocators = material_bind_group_allocator.into_inner(); let Some(material_instance) = material_instances.instances.get(&item.main_entity()) else { return RenderCommandResult::Skip; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_instance.asset_id.type_id()) + else { return RenderCommandResult::Skip; }; - let Some(material) = materials.get(material_asset_id) else { + let Some(material) = materials.get(material_instance.asset_id) else { return RenderCommandResult::Skip; }; let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) @@ -606,7 +561,7 @@ pub struct RenderMaterialInstances { /// A monotonically-increasing counter, which we use to sweep /// [`RenderMaterialInstances::instances`] when the entities and/or required /// components are removed. - current_change_tick: Tick, + pub current_change_tick: Tick, } impl RenderMaterialInstances { @@ -630,10 +585,10 @@ impl RenderMaterialInstances { /// material type, for simplicity. pub struct RenderMaterialInstance { /// The material asset. - pub(crate) asset_id: UntypedAssetId, + pub asset_id: UntypedAssetId, /// The [`RenderMaterialInstances::current_change_tick`] at which this /// material instance was last modified. - last_change_tick: Tick, + pub last_change_tick: Tick, } /// A [`SystemSet`] that contains all `extract_mesh_materials` systems. @@ -813,14 +768,14 @@ pub(crate) fn late_sweep_material_instances( pub fn extract_entities_needs_specialization( entities_needing_specialization: Extract>>, - mut entity_specialization_ticks: ResMut>, + mut entity_specialization_ticks: ResMut, mut removed_mesh_material_components: Extract>>, - mut specialized_material_pipeline_cache: ResMut>, + mut specialized_material_pipeline_cache: ResMut, mut specialized_prepass_material_pipeline_cache: Option< - ResMut>, + ResMut, >, mut specialized_shadow_material_pipeline_cache: Option< - ResMut>, + ResMut, >, views: Query<&ExtractedView>, ticks: SystemChangeTick, @@ -875,57 +830,27 @@ impl Default for EntitiesNeedingSpecialization { } } -#[derive(Resource, Deref, DerefMut, Clone, Debug)] -pub struct EntitySpecializationTicks { +#[derive(Resource, Deref, DerefMut, Default, Clone, Debug)] +pub struct EntitySpecializationTicks { #[deref] pub entities: MainEntityHashMap, - _marker: PhantomData, -} - -impl Default for EntitySpecializationTicks { - fn default() -> Self { - Self { - entities: MainEntityHashMap::default(), - _marker: Default::default(), - } - } } /// Stores the [`SpecializedMaterialViewPipelineCache`] for each view. -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedMaterialPipelineCache { // view entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } /// Stores the cached render pipeline ID for each entity in a single view, as /// well as the last time it was changed. -#[derive(Deref, DerefMut)] -pub struct SpecializedMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedMaterialViewPipelineCache { // material entity -> (tick, pipeline_id) #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: MainEntityHashMap::default(), - marker: PhantomData, - } - } } pub fn check_entities_needing_specialization( @@ -955,21 +880,19 @@ pub fn check_entities_needing_specialization( par_local.drain_into(&mut entities_needing_specialization); } -pub fn specialize_material_meshes( +pub fn specialize_material_meshes( render_meshes: Res>, - render_materials: Res>>, + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, render_lightmaps: Res, render_visibility_ranges: Res, ( - material_bind_group_allocator, opaque_render_phases, alpha_mask_render_phases, transmissive_render_phases, transparent_render_phases, ): ( - Res>, Res>, Res>, Res>, @@ -977,16 +900,14 @@ pub fn specialize_material_meshes( ), views: Query<(&ExtractedView, &RenderVisibleEntities)>, view_key_cache: Res, - entity_specialization_ticks: Res>, + entity_specialization_ticks: Res, view_specialization_ticks: Res, - mut specialized_material_pipeline_cache: ResMut>, - mut pipelines: ResMut>>, - pipeline: Res>, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, pipeline_cache: Res, ticks: SystemChangeTick, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { // Record the retained IDs of all shadow views so that we can expire old // pipeline IDs. let mut all_views: HashSet = HashSet::default(); @@ -1018,9 +939,6 @@ pub fn specialize_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; @@ -1039,12 +957,7 @@ pub fn specialize_material_meshes( let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { - continue; - }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; @@ -1085,13 +998,21 @@ pub fn specialize_material_meshes( } } - let key = MaterialPipelineKey { + let erased_key = ErasedMaterialPipelineKey { + type_id: material_instance.asset_id.type_id(), mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), + material_key: material.properties.material_key.clone(), }; - let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, key, &mesh.layout); + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material_pipeline_specializer, + erased_key, + &mesh.layout, + ); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { @@ -1112,8 +1033,8 @@ pub fn specialize_material_meshes( /// For each view, iterates over all the meshes visible from that view and adds /// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. -pub fn queue_material_meshes( - render_materials: Res>>, +pub fn queue_material_meshes( + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, mesh_allocator: Res, @@ -1123,10 +1044,8 @@ pub fn queue_material_meshes( mut transmissive_render_phases: ResMut>, mut transparent_render_phases: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities)>, - specialized_material_pipeline_cache: ResMut>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ + specialized_material_pipeline_cache: ResMut, +) { for (view, visible_entities) in &views { let ( Some(opaque_phase), @@ -1169,19 +1088,20 @@ pub fn queue_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; // Fetch the slabs that this mesh resides in. let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let Some(draw_function) = material.properties.get_draw_function(MaterialDrawFunction) + else { + continue; + }; match material.properties.render_phase_type { RenderPhaseType::Transmissive => { @@ -1189,7 +1109,7 @@ pub fn queue_material_meshes( + material.properties.depth_bias; transmissive_phase.add(Transmissive3d { entity: (*render_entity, *visible_entity), - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, distance, batch_range: 0..1, @@ -1208,7 +1128,7 @@ pub fn queue_material_meshes( } let batch_set_key = Opaque3dBatchSetKey { pipeline: pipeline_id, - draw_function: material.properties.draw_function_id, + draw_function, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, @@ -1232,7 +1152,7 @@ pub fn queue_material_meshes( // Alpha mask RenderPhaseType::AlphaMask => { let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1258,7 +1178,7 @@ pub fn queue_material_meshes( + material.properties.depth_bias; transparent_phase.add(Transparent3d { entity: (*render_entity, *visible_entity), - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, distance, batch_range: 0..1, @@ -1321,7 +1241,47 @@ pub enum OpaqueRendererMethod { Auto, } +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletPrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletDeferredFragmentShader; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct ShadowsDrawFunction; + /// Common [`Material`] properties, calculated for a specific material instance. +#[derive(Default)] pub struct MaterialProperties { /// Is this material should be rendered by the deferred renderer when. /// [`AlphaMode::Opaque`] or [`AlphaMode::Mask`] @@ -1343,13 +1303,65 @@ pub struct MaterialProperties { /// rendering to take place in a separate [`Transmissive3d`] pass. pub reads_view_transmission_texture: bool, pub render_phase_type: RenderPhaseType, - pub draw_function_id: DrawFunctionId, - pub prepass_draw_function_id: Option, - pub deferred_draw_function_id: Option, + pub material_layout: Option, + /// Backing array is a size of 4 because the `StandardMaterial` needs 4 draw functions by default + pub draw_functions: SmallVec<[(InternedDrawFunctionLabel, DrawFunctionId); 4]>, + /// Backing array is a size of 3 because the `StandardMaterial` has 3 custom shaders (`frag`, `prepass_frag`, `deferred_frag`) which is the + /// most common use case + pub shaders: SmallVec<[(InternedShaderLabel, Handle); 3]>, + /// Whether this material *actually* uses bindless resources, taking the + /// platform support (or lack thereof) of bindless resources into account. + pub bindless: bool, + pub specialize: Option< + fn( + &MaterialPipeline, + &mut RenderPipelineDescriptor, + &MeshVertexBufferLayoutRef, + ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError>, + >, + /// The key for this material, typically a bitfield of flags that are used to modify + /// the pipeline descriptor used for this material. + pub material_key: SmallVec<[u8; 8]>, + /// Whether shadows are enabled for this material + pub shadows_enabled: bool, + /// Whether prepass is enabled for this material + pub prepass_enabled: bool, } -#[derive(Clone, Copy)] +impl MaterialProperties { + pub fn get_shader(&self, label: impl ShaderLabel) -> Option> { + self.shaders + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_shader(&mut self, label: impl ShaderLabel, shader: Handle) { + self.shaders.push((label.intern(), shader)); + } + + pub fn get_draw_function(&self, label: impl DrawFunctionLabel) -> Option { + self.draw_functions + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_draw_function( + &mut self, + label: impl DrawFunctionLabel, + draw_function: DrawFunctionId, + ) { + self.draw_functions.push((label.intern(), draw_function)); + } +} + +#[derive(Clone, Copy, Default)] pub enum RenderPhaseType { + #[default] Opaque, AlphaMask, Transmissive, @@ -1365,20 +1377,23 @@ pub enum RenderPhaseType { pub struct RenderMaterialBindings(HashMap); /// Data prepared for a [`Material`] instance. -pub struct PreparedMaterial { +pub struct PreparedMaterial { pub binding: MaterialBindingId, - pub properties: MaterialProperties, - pub phantom: PhantomData, + pub properties: Arc, } -impl RenderAsset for PreparedMaterial { +// orphan rules T_T +impl ErasedRenderAsset for MeshMaterial3d +where + M::Data: Clone, +{ type SourceAsset = M; + type ErasedAsset = PreparedMaterial; type Param = ( SRes, - SRes>, SRes, - SResMut>, + SResMut, SResMut, SRes>, SRes>, @@ -1388,7 +1403,13 @@ impl RenderAsset for PreparedMaterial { SRes>, SRes>, SRes>, - M::Param, + SRes>, + SRes, + ( + Option>>, + Option>>, + M::Param, + ), ); fn prepare_asset( @@ -1396,9 +1417,8 @@ impl RenderAsset for PreparedMaterial { material_id: AssetId, ( render_device, - pipeline, default_opaque_render_method, - bind_group_allocator, + bind_group_allocators, render_material_bindings, opaque_draw_functions, alpha_mask_draw_functions, @@ -1408,25 +1428,31 @@ impl RenderAsset for PreparedMaterial { alpha_mask_prepass_draw_functions, opaque_deferred_draw_functions, alpha_mask_deferred_draw_functions, - material_param, + shadow_draw_functions, + asset_server, + (shadows_enabled, prepass_enabled, material_param), ): &mut SystemParamItem, - ) -> Result> { - let draw_opaque_pbr = opaque_draw_functions.read().id::>(); - let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); - let draw_transmissive_pbr = transmissive_draw_functions.read().id::>(); - let draw_transparent_pbr = transparent_draw_functions.read().id::>(); - let draw_opaque_prepass = opaque_prepass_draw_functions - .read() - .get_id::>(); + ) -> Result> { + let material_layout = M::bind_group_layout(render_device); + + let shadows_enabled = shadows_enabled.is_some(); + let prepass_enabled = prepass_enabled.is_some(); + + let draw_opaque_pbr = opaque_draw_functions.read().id::(); + let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::(); + let draw_transmissive_pbr = transmissive_draw_functions.read().id::(); + let draw_transparent_pbr = transparent_draw_functions.read().id::(); + let draw_opaque_prepass = opaque_prepass_draw_functions.read().get_id::(); let draw_alpha_mask_prepass = alpha_mask_prepass_draw_functions .read() - .get_id::>(); + .get_id::(); let draw_opaque_deferred = opaque_deferred_draw_functions .read() - .get_id::>(); + .get_id::(); let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions .read() - .get_id::>(); + .get_id::(); + let shadow_draw_function_id = shadow_draw_functions.read().get_id::(); let render_method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, @@ -1469,13 +1495,81 @@ impl RenderAsset for PreparedMaterial { _ => None, }; - match material.unprepared_bind_group( - &pipeline.material_layout, - render_device, - material_param, - false, - ) { + let mut draw_functions = SmallVec::new(); + draw_functions.push((MaterialDrawFunction.intern(), draw_function_id)); + if let Some(prepass_draw_function_id) = prepass_draw_function_id { + draw_functions.push((PrepassDrawFunction.intern(), prepass_draw_function_id)); + } + if let Some(deferred_draw_function_id) = deferred_draw_function_id { + draw_functions.push((DeferredDrawFunction.intern(), deferred_draw_function_id)); + } + if let Some(shadow_draw_function_id) = shadow_draw_function_id { + draw_functions.push((ShadowsDrawFunction.intern(), shadow_draw_function_id)); + } + + let mut shaders = SmallVec::new(); + let mut add_shader = |label: InternedShaderLabel, shader_ref: ShaderRef| { + let mayber_shader = match shader_ref { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }; + if let Some(shader) = mayber_shader { + shaders.push((label, shader)); + } + }; + add_shader(MaterialVertexShader.intern(), M::vertex_shader()); + add_shader(MaterialFragmentShader.intern(), M::fragment_shader()); + add_shader(PrepassVertexShader.intern(), M::prepass_vertex_shader()); + add_shader(PrepassFragmentShader.intern(), M::prepass_fragment_shader()); + add_shader(DeferredVertexShader.intern(), M::deferred_vertex_shader()); + add_shader( + DeferredFragmentShader.intern(), + M::deferred_fragment_shader(), + ); + + #[cfg(feature = "meshlet")] + { + add_shader( + MeshletFragmentShader.intern(), + M::meshlet_mesh_fragment_shader(), + ); + add_shader( + MeshletPrepassFragmentShader.intern(), + M::meshlet_mesh_prepass_fragment_shader(), + ); + add_shader( + MeshletDeferredFragmentShader.intern(), + M::meshlet_mesh_deferred_fragment_shader(), + ); + } + + let bindless = material_uses_bindless_resources::(render_device); + let bind_group_data = material.bind_group_data(); + let material_key = SmallVec::from(bytemuck::bytes_of(&bind_group_data)); + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + mesh_layout: &MeshVertexBufferLayoutRef, + erased_key: ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + let material_key = bytemuck::from_bytes(erased_key.material_key.as_slice()); + M::specialize( + pipeline, + descriptor, + mesh_layout, + MaterialPipelineKey { + mesh_key: erased_key.mesh_key, + bind_group_data: *material_key, + }, + ) + } + + match material.unprepared_bind_group(&material_layout, render_device, material_param, false) + { Ok(unprepared) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); // Allocate or update the material. let binding = match render_material_bindings.entry(material_id.into()) { Entry::Occupied(mut occupied_entry) => { @@ -1484,31 +1578,34 @@ impl RenderAsset for PreparedMaterial { // change. For now, we just delete and recreate the bind // group. bind_group_allocator.free(*occupied_entry.get()); - let new_binding = bind_group_allocator - .allocate_unprepared(unprepared, &pipeline.material_layout); + let new_binding = + bind_group_allocator.allocate_unprepared(unprepared, &material_layout); *occupied_entry.get_mut() = new_binding; new_binding } Entry::Vacant(vacant_entry) => *vacant_entry.insert( - bind_group_allocator - .allocate_unprepared(unprepared, &pipeline.material_layout), + bind_group_allocator.allocate_unprepared(unprepared, &material_layout), ), }; Ok(PreparedMaterial { binding, - properties: MaterialProperties { + properties: Arc::new(MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, - draw_function_id, - prepass_draw_function_id, render_method, mesh_pipeline_key_bits, - deferred_draw_function_id, - }, - phantom: PhantomData, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), }) } @@ -1521,12 +1618,10 @@ impl RenderAsset for PreparedMaterial { // and is requesting a fully-custom bind group. Invoke // `as_bind_group` as requested, and store the resulting bind // group in the slot. - match material.as_bind_group( - &pipeline.material_layout, - render_device, - material_param, - ) { + match material.as_bind_group(&material_layout, render_device, material_param) { Ok(prepared_bind_group) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); // Store the resulting bind group directly in the slot. let material_binding_id = bind_group_allocator.allocate_prepared(prepared_bind_group); @@ -1534,18 +1629,22 @@ impl RenderAsset for PreparedMaterial { Ok(PreparedMaterial { binding: material_binding_id, - properties: MaterialProperties { + properties: Arc::new(MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, - draw_function_id, - prepass_draw_function_id, render_method, mesh_pipeline_key_bits, - deferred_draw_function_id, - }, - phantom: PhantomData, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), }) } @@ -1563,7 +1662,7 @@ impl RenderAsset for PreparedMaterial { fn unload_asset( source_asset: AssetId, - (_, _, _, bind_group_allocator, render_material_bindings, ..): &mut SystemParamItem< + (_, _, bind_group_allocators, render_material_bindings, ..): &mut SystemParamItem< Self::Param, >, ) { @@ -1571,7 +1670,8 @@ impl RenderAsset for PreparedMaterial { else { return; }; - bind_group_allocator.free(material_binding_id); + let bind_group_allactor = bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + bind_group_allactor.free(material_binding_id); } } @@ -1592,15 +1692,15 @@ impl From for MaterialBindGroupId { /// Creates and/or recreates any bind groups that contain materials that were /// modified this frame. -pub fn prepare_material_bind_groups( - mut allocator: ResMut>, +pub fn prepare_material_bind_groups( + mut allocators: ResMut, render_device: Res, fallback_image: Res, fallback_resources: Res, -) where - M: Material, -{ - allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); +) { + for (_, allocator) in allocators.iter_mut() { + allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); + } } /// Uploads the contents of all buffers that the [`MaterialBindGroupAllocator`] @@ -1608,12 +1708,22 @@ pub fn prepare_material_bind_groups( /// /// Non-bindless allocators don't currently manage any buffers, so this method /// only has an effect for bindless allocators. -pub fn write_material_bind_group_buffers( - mut allocator: ResMut>, +pub fn write_material_bind_group_buffers( + mut allocators: ResMut, render_device: Res, render_queue: Res, -) where - M: Material, -{ - allocator.write_buffers(&render_device, &render_queue); +) { + for (_, allocator) in allocators.iter_mut() { + allocator.write_buffers(&render_device, &render_queue); + } +} + +/// Marker resource for whether shadows are enabled for this material type +#[derive(Resource, Debug)] +pub struct ShadowsEnabled(PhantomData); + +impl Default for ShadowsEnabled { + fn default() -> Self { + Self(PhantomData) + } } diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index b539d2098f..780ac8e10c 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -4,8 +4,7 @@ //! allocator manages each bind group, assigning slots to materials as //! appropriate. -use core::{cmp::Ordering, iter, marker::PhantomData, mem, ops::Range}; - +use crate::Material; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ resource::Resource, @@ -13,6 +12,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::render_resource::BindlessSlabResourceLimit; use bevy_render::{ render_resource::{ BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource, @@ -26,11 +26,14 @@ use bevy_render::{ settings::WgpuFeatures, texture::FallbackImage, }; -use bevy_utils::default; +use bevy_utils::{default, TypeIdMap}; use bytemuck::Pod; +use core::hash::Hash; +use core::{cmp::Ordering, iter, mem, ops::Range}; use tracing::{error, trace}; -use crate::Material; +#[derive(Resource, Deref, DerefMut, Default)] +pub struct MaterialBindGroupAllocators(TypeIdMap); /// A resource that places materials into bind groups and tracks their /// resources. @@ -38,25 +41,20 @@ use crate::Material; /// Internally, Bevy has separate allocators for bindless and non-bindless /// materials. This resource provides a common interface to the specific /// allocator in use. -#[derive(Resource)] -pub enum MaterialBindGroupAllocator -where - M: Material, -{ +pub enum MaterialBindGroupAllocator { /// The allocator used when the material is bindless. - Bindless(Box>), + Bindless(Box), /// The allocator used when the material is non-bindless. - NonBindless(Box>), + NonBindless(Box), } /// The allocator that places bindless materials into bind groups and tracks /// their resources. -pub struct MaterialBindGroupBindlessAllocator -where - M: Material, -{ +pub struct MaterialBindGroupBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, /// The slabs, each of which contains a bind group. - slabs: Vec>, + slabs: Vec, /// The layout of the bind groups that we produce. bind_group_layout: BindGroupLayout, /// Information about the bindless resources in the material. @@ -79,10 +77,7 @@ where } /// A single bind group and the bookkeeping necessary to allocate into it. -pub struct MaterialBindlessSlab -where - M: Material, -{ +pub struct MaterialBindlessSlab { /// The current bind group, if it's up to date. /// /// If this is `None`, then the bind group is dirty and needs to be @@ -98,7 +93,7 @@ where /// /// Because the slab binary searches this table, the entries within must be /// sorted by bindless index. - bindless_index_tables: Vec>, + bindless_index_tables: Vec, /// The binding arrays containing samplers. samplers: HashMap>, @@ -110,12 +105,6 @@ where /// `#[data]` attribute of `AsBindGroup`). data_buffers: HashMap, - /// Holds extra CPU-accessible data that the material provides. - /// - /// Typically, this data is used for constructing the material key, for - /// pipeline specialization purposes. - extra_data: Vec>, - /// A list of free slot IDs. free_slots: Vec, /// The total number of materials currently allocated in this slab. @@ -130,10 +119,7 @@ where /// This is conventionally assigned to bind group binding 0, but it can be /// changed by altering the [`Self::binding_number`], which corresponds to the /// `#[bindless(index_table(binding(B)))]` attribute in `AsBindGroup`. -struct MaterialBindlessIndexTable -where - M: Material, -{ +struct MaterialBindlessIndexTable { /// The buffer containing the mappings. buffer: RetainedRawBufferVec, /// The range of bindless indices that this bindless index table covers. @@ -146,7 +132,6 @@ where index_range: Range, /// The binding number that this index table is assigned to in the shader. binding_number: BindingNumber, - phantom: PhantomData, } /// A single binding array for storing bindless resources and the bookkeeping @@ -189,13 +174,12 @@ where } /// The allocator that stores bind groups for non-bindless materials. -pub struct MaterialBindGroupNonBindlessAllocator -where - M: Material, -{ +pub struct MaterialBindGroupNonBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, /// A mapping from [`MaterialBindGroupIndex`] to the bind group allocated in /// each slot. - bind_groups: Vec>>, + bind_groups: Vec>, /// The bind groups that are dirty and need to be prepared. /// /// To prepare the bind groups, call @@ -203,15 +187,11 @@ where to_prepare: HashSet, /// A list of free bind group indices. free_indices: Vec, - phantom: PhantomData, } /// A single bind group that a [`MaterialBindGroupNonBindlessAllocator`] is /// currently managing. -enum MaterialNonBindlessAllocatedBindGroup -where - M: Material, -{ +enum MaterialNonBindlessAllocatedBindGroup { /// An unprepared bind group. /// /// The allocator prepares all outstanding unprepared bind groups when @@ -219,13 +199,13 @@ where /// called. Unprepared { /// The unprepared bind group, including extra data. - bind_group: UnpreparedBindGroup, + bind_group: UnpreparedBindGroup, /// The layout of that bind group. layout: BindGroupLayout, }, /// A bind group that's already been prepared. Prepared { - bind_group: PreparedBindGroup, + bind_group: PreparedBindGroup, #[expect(dead_code, reason = "These buffers are only referenced by bind groups")] uniform_buffers: Vec, }, @@ -351,35 +331,27 @@ trait GetBindingResourceId { } /// The public interface to a slab, which represents a single bind group. -pub struct MaterialSlab<'a, M>(MaterialSlabImpl<'a, M>) -where - M: Material; +pub struct MaterialSlab<'a>(MaterialSlabImpl<'a>); /// The actual implementation of a material slab. /// /// This has bindless and non-bindless variants. -enum MaterialSlabImpl<'a, M> -where - M: Material, -{ +enum MaterialSlabImpl<'a> { /// The implementation of the slab interface we use when the slab /// is bindless. - Bindless(&'a MaterialBindlessSlab), + Bindless(&'a MaterialBindlessSlab), /// The implementation of the slab interface we use when the slab /// is non-bindless. - NonBindless(MaterialNonBindlessSlab<'a, M>), + NonBindless(MaterialNonBindlessSlab<'a>), } /// A single bind group that the [`MaterialBindGroupNonBindlessAllocator`] /// manages. -enum MaterialNonBindlessSlab<'a, M> -where - M: Material, -{ +enum MaterialNonBindlessSlab<'a> { /// A slab that has a bind group. - Prepared(&'a PreparedBindGroup), + Prepared(&'a PreparedBindGroup), /// A slab that doesn't yet have a bind group. - Unprepared(&'a UnpreparedBindGroup), + Unprepared, } /// Manages an array of untyped plain old data on GPU and allocates individual @@ -480,26 +452,33 @@ impl GetBindingResourceId for TextureView { } } -impl MaterialBindGroupAllocator -where - M: Material, -{ +impl MaterialBindGroupAllocator { /// Creates a new [`MaterialBindGroupAllocator`] managing the data for a /// single material. - fn new(render_device: &RenderDevice) -> MaterialBindGroupAllocator { - if material_uses_bindless_resources::(render_device) { + pub fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: Option, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupAllocator { + if let Some(bindless_descriptor) = bindless_descriptor { MaterialBindGroupAllocator::Bindless(Box::new(MaterialBindGroupBindlessAllocator::new( render_device, + label, + bindless_descriptor, + bind_group_layout, + slab_capacity, ))) } else { MaterialBindGroupAllocator::NonBindless(Box::new( - MaterialBindGroupNonBindlessAllocator::new(), + MaterialBindGroupNonBindlessAllocator::new(label), )) } } /// Returns the slab with the given index, if one exists. - pub fn get(&self, group: MaterialBindGroupIndex) -> Option> { + pub fn get(&self, group: MaterialBindGroupIndex) -> Option { match *self { MaterialBindGroupAllocator::Bindless(ref bindless_allocator) => bindless_allocator .get(group) @@ -520,7 +499,7 @@ where /// you need to prepare the bind group yourself. pub fn allocate_unprepared( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, bind_group_layout: &BindGroupLayout, ) -> MaterialBindingId { match *self { @@ -545,7 +524,7 @@ where /// this method if you need to prepare the bind group yourself. pub fn allocate_prepared( &mut self, - prepared_bind_group: PreparedBindGroup, + prepared_bind_group: PreparedBindGroup, ) -> MaterialBindingId { match *self { MaterialBindGroupAllocator::Bindless(_) => { @@ -613,14 +592,11 @@ where } } -impl MaterialBindlessIndexTable -where - M: Material, -{ +impl MaterialBindlessIndexTable { /// Creates a new [`MaterialBindlessIndexTable`] for a single slab. fn new( bindless_index_table_descriptor: &BindlessIndexTableDescriptor, - ) -> MaterialBindlessIndexTable { + ) -> MaterialBindlessIndexTable { // Preallocate space for one bindings table, so that there will always be a buffer. let mut buffer = RetainedRawBufferVec::new(BufferUsages::STORAGE); for _ in *bindless_index_table_descriptor.indices.start @@ -633,7 +609,6 @@ where buffer, index_range: bindless_index_table_descriptor.indices.clone(), binding_number: bindless_index_table_descriptor.binding_number, - phantom: PhantomData, } } @@ -747,15 +722,16 @@ where } } -impl MaterialBindGroupBindlessAllocator -where - M: Material, -{ +impl MaterialBindGroupBindlessAllocator { /// Creates a new [`MaterialBindGroupBindlessAllocator`] managing the data /// for a single bindless material. - fn new(render_device: &RenderDevice) -> MaterialBindGroupBindlessAllocator { - let bindless_descriptor = M::bindless_descriptor() - .expect("Non-bindless materials should use the non-bindless allocator"); + fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: BindlessDescriptor, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupBindlessAllocator { let fallback_buffers = bindless_descriptor .buffers .iter() @@ -776,11 +752,12 @@ where .collect(); MaterialBindGroupBindlessAllocator { + label, slabs: vec![], - bind_group_layout: M::bind_group_layout(render_device), + bind_group_layout, bindless_descriptor, fallback_buffers, - slab_capacity: M::bindless_slot_count() + slab_capacity: slab_capacity .expect("Non-bindless materials should use the non-bindless allocator") .resolve(), } @@ -796,7 +773,7 @@ where /// created, and the material is allocated into it. fn allocate_unprepared( &mut self, - mut unprepared_bind_group: UnpreparedBindGroup, + mut unprepared_bind_group: UnpreparedBindGroup, ) -> MaterialBindingId { for (slab_index, slab) in self.slabs.iter_mut().enumerate() { trace!("Trying to allocate in slab {}", slab_index); @@ -842,7 +819,7 @@ where /// /// A [`MaterialBindGroupIndex`] can be fetched from a /// [`MaterialBindingId`]. - fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { + fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { self.slabs.get(group.0 as usize) } @@ -858,6 +835,7 @@ where for slab in &mut self.slabs { slab.prepare( render_device, + self.label, &self.bind_group_layout, fallback_bindless_resources, &self.fallback_buffers, @@ -878,20 +856,7 @@ where } } -impl FromWorld for MaterialBindGroupAllocator -where - M: Material, -{ - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - MaterialBindGroupAllocator::new(render_device) - } -} - -impl MaterialBindlessSlab -where - M: Material, -{ +impl MaterialBindlessSlab { /// Attempts to allocate the given unprepared bind group in this slab. /// /// If the allocation succeeds, this method returns the slot that the @@ -900,9 +865,9 @@ where /// so that it can try to allocate again. fn try_allocate( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, slot_capacity: u32, - ) -> Result> { + ) -> Result { // Locate pre-existing resources, and determine how many free slots we need. let Some(allocation_candidate) = self.check_allocation(&unprepared_bind_group) else { return Err(unprepared_bind_group); @@ -942,12 +907,6 @@ where bindless_index_table.set(slot, &allocated_resource_slots); } - // Insert extra data. - if self.extra_data.len() < (*slot as usize + 1) { - self.extra_data.resize_with(*slot as usize + 1, || None); - } - self.extra_data[*slot as usize] = Some(unprepared_bind_group.data); - // Invalidate the cached bind group. self.bind_group = None; @@ -958,7 +917,7 @@ where /// bind group can be allocated in this slab. fn check_allocation( &self, - unprepared_bind_group: &UnpreparedBindGroup, + unprepared_bind_group: &UnpreparedBindGroup, ) -> Option { let mut allocation_candidate = BindlessAllocationCandidate { pre_existing_resources: HashMap::default(), @@ -1048,63 +1007,14 @@ where for (bindless_index, owned_binding_resource) in binding_resources.drain(..) { let bindless_index = BindlessIndex(bindless_index); - // If this is an other reference to an object we've already - // allocated, just bump its reference count. - if let Some(pre_existing_resource_slot) = allocation_candidate + + let pre_existing_slot = allocation_candidate .pre_existing_resources - .get(&bindless_index) - { - allocated_resource_slots.insert(bindless_index, *pre_existing_resource_slot); - - match owned_binding_resource { - OwnedBindingResource::Buffer(_) => { - self.buffers - .get_mut(&bindless_index) - .expect("Buffer binding array should exist") - .bindings - .get_mut(*pre_existing_resource_slot as usize) - .and_then(|binding| binding.as_mut()) - .expect("Slot should exist") - .ref_count += 1; - } - - OwnedBindingResource::Data(_) => { - panic!("Data buffers can't be deduplicated") - } - - OwnedBindingResource::TextureView(texture_view_dimension, _) => { - let bindless_resource_type = - BindlessResourceType::from(texture_view_dimension); - self.textures - .get_mut(&bindless_resource_type) - .expect("Texture binding array should exist") - .bindings - .get_mut(*pre_existing_resource_slot as usize) - .and_then(|binding| binding.as_mut()) - .expect("Slot should exist") - .ref_count += 1; - } - - OwnedBindingResource::Sampler(sampler_binding_type, _) => { - let bindless_resource_type = - BindlessResourceType::from(sampler_binding_type); - self.samplers - .get_mut(&bindless_resource_type) - .expect("Sampler binding array should exist") - .bindings - .get_mut(*pre_existing_resource_slot as usize) - .and_then(|binding| binding.as_mut()) - .expect("Slot should exist") - .ref_count += 1; - } - } - - continue; - } + .get(&bindless_index); // Otherwise, we need to insert it anew. let binding_resource_id = BindingResourceId::from(&owned_binding_resource); - match owned_binding_resource { + let increment_allocated_resource_count = match owned_binding_resource { OwnedBindingResource::Buffer(buffer) => { let slot = self .buffers @@ -1112,14 +1022,27 @@ where .expect("Buffer binding array should exist") .insert(binding_resource_id, buffer); allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } } OwnedBindingResource::Data(data) => { + if pre_existing_slot.is_some() { + panic!("Data buffers can't be deduplicated") + } + let slot = self .data_buffers .get_mut(&bindless_index) .expect("Data buffer binding array should exist") .insert(&data); allocated_resource_slots.insert(bindless_index, slot); + false } OwnedBindingResource::TextureView(texture_view_dimension, texture_view) => { let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); @@ -1129,6 +1052,14 @@ where .expect("Texture array should exist") .insert(binding_resource_id, texture_view); allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } } OwnedBindingResource::Sampler(sampler_binding_type, sampler) => { let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); @@ -1138,11 +1069,21 @@ where .expect("Sampler should exist") .insert(binding_resource_id, sampler); allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } } - } + }; // Bump the allocated resource count. - self.allocated_resource_count += 1; + if increment_allocated_resource_count { + self.allocated_resource_count += 1; + } } allocated_resource_slots @@ -1207,9 +1148,6 @@ where } } - // Clear out the extra data. - self.extra_data[slot.0 as usize] = None; - // Invalidate the cached bind group. self.bind_group = None; @@ -1222,6 +1160,7 @@ where fn prepare( &mut self, render_device: &RenderDevice, + label: Option<&'static str>, bind_group_layout: &BindGroupLayout, fallback_bindless_resources: &FallbackBindlessResources, fallback_buffers: &HashMap, @@ -1242,6 +1181,7 @@ where // Create the bind group if needed. self.prepare_bind_group( render_device, + label, bind_group_layout, fallback_bindless_resources, fallback_buffers, @@ -1256,6 +1196,7 @@ where fn prepare_bind_group( &mut self, render_device: &RenderDevice, + label: Option<&'static str>, bind_group_layout: &BindGroupLayout, fallback_bindless_resources: &FallbackBindlessResources, fallback_buffers: &HashMap, @@ -1322,11 +1263,8 @@ where }); } - self.bind_group = Some(render_device.create_bind_group( - M::label(), - bind_group_layout, - &bind_group_entries, - )); + self.bind_group = + Some(render_device.create_bind_group(label, bind_group_layout, &bind_group_entries)); } /// Writes any buffers that we're managing to the GPU. @@ -1566,19 +1504,11 @@ where self.bind_group.as_ref() } - /// Returns the extra data associated with this material. - fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - self.extra_data - .get(slot.0 as usize) - .and_then(|data| data.as_ref()) - .expect("Extra data not present") - } - /// Returns the bindless index table containing the given bindless index. fn get_bindless_index_table( &self, bindless_index: BindlessIndex, - ) -> Option<&MaterialBindlessIndexTable> { + ) -> Option<&MaterialBindlessIndexTable> { let table_index = self .bindless_index_tables .binary_search_by(|bindless_index_table| { @@ -1626,16 +1556,30 @@ where /// Inserts a bindless resource into a binding array and returns the index /// of the slot it was inserted into. fn insert(&mut self, binding_resource_id: BindingResourceId, resource: R) -> u32 { - let slot = self.free_slots.pop().unwrap_or(self.len); - self.resource_to_slot.insert(binding_resource_id, slot); + match self.resource_to_slot.entry(binding_resource_id) { + bevy_platform::collections::hash_map::Entry::Occupied(o) => { + let slot = *o.get(); - if self.bindings.len() < slot as usize + 1 { - self.bindings.resize_with(slot as usize + 1, || None); + self.bindings[slot as usize] + .as_mut() + .expect("A slot in the resource_to_slot map should have a value") + .ref_count += 1; + + slot + } + bevy_platform::collections::hash_map::Entry::Vacant(v) => { + let slot = self.free_slots.pop().unwrap_or(self.len); + v.insert(slot); + + if self.bindings.len() < slot as usize + 1 { + self.bindings.resize_with(slot as usize + 1, || None); + } + self.bindings[slot as usize] = Some(MaterialBindlessBinding::new(resource)); + + self.len += 1; + slot + } } - self.bindings[slot as usize] = Some(MaterialBindlessBinding::new(resource)); - - self.len += 1; - slot } /// Removes a reference to an object from the slot. @@ -1693,15 +1637,12 @@ where }) } -impl MaterialBindlessSlab -where - M: Material, -{ +impl MaterialBindlessSlab { /// Creates a new [`MaterialBindlessSlab`] for a material with the given /// bindless descriptor. /// /// We use this when no existing slab could hold a material to be allocated. - fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { + fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { let mut buffers = HashMap::default(); let mut samplers = HashMap::default(); let mut textures = HashMap::default(); @@ -1784,7 +1725,7 @@ where let bindless_index_tables = bindless_descriptor .index_tables .iter() - .map(|bindless_index_table| MaterialBindlessIndexTable::new(bindless_index_table)) + .map(MaterialBindlessIndexTable::new) .collect(); MaterialBindlessSlab { @@ -1794,7 +1735,6 @@ where textures, buffers, data_buffers, - extra_data: vec![], free_slots: vec![], live_allocation_count: 0, allocated_resource_count: 0, @@ -1826,18 +1766,15 @@ impl FromWorld for FallbackBindlessResources { } } -impl MaterialBindGroupNonBindlessAllocator -where - M: Material, -{ +impl MaterialBindGroupNonBindlessAllocator { /// Creates a new [`MaterialBindGroupNonBindlessAllocator`] managing the /// bind groups for a single non-bindless material. - fn new() -> MaterialBindGroupNonBindlessAllocator { + fn new(label: Option<&'static str>) -> MaterialBindGroupNonBindlessAllocator { MaterialBindGroupNonBindlessAllocator { + label, bind_groups: vec![], to_prepare: HashSet::default(), free_indices: vec![], - phantom: PhantomData, } } @@ -1846,10 +1783,7 @@ where /// /// The returned [`MaterialBindingId`] can later be used to fetch the bind /// group. - fn allocate( - &mut self, - bind_group: MaterialNonBindlessAllocatedBindGroup, - ) -> MaterialBindingId { + fn allocate(&mut self, bind_group: MaterialNonBindlessAllocatedBindGroup) -> MaterialBindingId { let group_id = self .free_indices .pop() @@ -1878,7 +1812,7 @@ where /// [`MaterialBindingId`]. fn allocate_unprepared( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, bind_group_layout: BindGroupLayout, ) -> MaterialBindingId { self.allocate(MaterialNonBindlessAllocatedBindGroup::Unprepared { @@ -1889,10 +1823,7 @@ where /// Inserts an prepared bind group into this allocator and returns a /// [`MaterialBindingId`]. - fn allocate_prepared( - &mut self, - prepared_bind_group: PreparedBindGroup, - ) -> MaterialBindingId { + fn allocate_prepared(&mut self, prepared_bind_group: PreparedBindGroup) -> MaterialBindingId { self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group: prepared_bind_group, uniform_buffers: vec![], @@ -1909,15 +1840,15 @@ where } /// Returns a wrapper around the bind group with the given index. - fn get(&self, group: MaterialBindGroupIndex) -> Option> { + fn get(&self, group: MaterialBindGroupIndex) -> Option { self.bind_groups[group.0 as usize] .as_ref() .map(|bind_group| match bind_group { MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group, .. } => { MaterialNonBindlessSlab::Prepared(bind_group) } - MaterialNonBindlessAllocatedBindGroup::Unprepared { bind_group, .. } => { - MaterialNonBindlessSlab::Unprepared(bind_group) + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } => { + MaterialNonBindlessSlab::Unprepared } }) } @@ -1976,7 +1907,7 @@ where // Create the bind group. let bind_group = render_device.create_bind_group( - M::label(), + self.label, &bind_group_layout, &bind_group_entries, ); @@ -1986,7 +1917,6 @@ where bind_group: PreparedBindGroup { bindings: unprepared_bind_group.bindings, bind_group, - data: unprepared_bind_group.data, }, uniform_buffers, }); @@ -1994,28 +1924,7 @@ where } } -impl<'a, M> MaterialSlab<'a, M> -where - M: Material, -{ - /// Returns the extra data associated with this material. - /// - /// When deriving `AsBindGroup`, this data is given by the - /// `#[bind_group_data(DataType)]` attribute on the material structure. - pub fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - match self.0 { - MaterialSlabImpl::Bindless(material_bindless_slab) => { - material_bindless_slab.get_extra_data(slot) - } - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( - prepared_bind_group, - )) => &prepared_bind_group.data, - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared( - unprepared_bind_group, - )) => &unprepared_bind_group.data, - } - } - +impl<'a> MaterialSlab<'a> { /// Returns the [`BindGroup`] corresponding to this slab, if it's been /// prepared. /// @@ -2030,7 +1939,7 @@ where MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( prepared_bind_group, )) => Some(&prepared_bind_group.bind_group), - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared(_)) => None, + MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared) => None, } } } @@ -2055,7 +1964,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/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index c158650d1b..584ea345e3 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -6,9 +6,9 @@ use bevy_asset::{ }; use bevy_math::{Vec2, Vec3}; use bevy_reflect::TypePath; +use bevy_render::render_resource::ShaderType; use bevy_tasks::block_on; use bytemuck::{Pod, Zeroable}; -use half::f16; use lz4_flex::frame::{FrameDecoder, FrameEncoder}; use std::io::{Read, Write}; use thiserror::Error; @@ -17,7 +17,7 @@ use thiserror::Error; const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668; /// The current version of the [`MeshletMesh`] asset format. -pub const MESHLET_MESH_ASSET_VERSION: u64 = 1; +pub const MESHLET_MESH_ASSET_VERSION: u64 = 2; /// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets. /// @@ -47,12 +47,32 @@ pub struct MeshletMesh { pub(crate) vertex_uvs: Arc<[Vec2]>, /// Triangle indices for meshlets. pub(crate) indices: Arc<[u8]>, + /// The BVH8 used for culling and LOD selection of the meshlets. The root is at index 0. + pub(crate) bvh: Arc<[BvhNode]>, /// The list of meshlets making up this mesh. pub(crate) meshlets: Arc<[Meshlet]>, /// Spherical bounding volumes. - pub(crate) meshlet_bounding_spheres: Arc<[MeshletBoundingSpheres]>, - /// Meshlet group and parent group simplification errors. - pub(crate) meshlet_simplification_errors: Arc<[MeshletSimplificationError]>, + pub(crate) meshlet_cull_data: Arc<[MeshletCullData]>, + /// The tight AABB of the meshlet mesh, used for frustum and occlusion culling at the instance + /// level. + pub(crate) aabb: MeshletAabb, + /// The depth of the culling BVH, used to determine the number of dispatches at runtime. + pub(crate) bvh_depth: u32, +} + +/// A single BVH8 node in the BVH used for culling and LOD selection of a [`MeshletMesh`]. +#[derive(Copy, Clone, Default, Pod, Zeroable)] +#[repr(C)] +pub struct BvhNode { + /// The tight AABBs of this node's children, used for frustum and occlusion during BVH + /// traversal. + pub aabbs: [MeshletAabbErrorOffset; 8], + /// The LOD bounding spheres of this node's children, used for LOD selection during BVH + /// traversal. + pub lod_bounds: [MeshletBoundingSphere; 8], + /// If `u8::MAX`, it indicates that the child of each children is a BVH node, otherwise it is the number of meshlets in the group. + pub child_counts: [u8; 8], + pub _padding: [u32; 2], } /// A single meshlet within a [`MeshletMesh`]. @@ -91,33 +111,39 @@ pub struct Meshlet { /// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`]. #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] -pub struct MeshletBoundingSpheres { - /// Bounding sphere used for frustum and occlusion culling for this meshlet. - pub culling_sphere: MeshletBoundingSphere, +pub struct MeshletCullData { + /// Tight bounding box, used for frustum and occlusion culling for this meshlet. + pub aabb: MeshletAabbErrorOffset, /// Bounding sphere used for determining if this meshlet's group is at the correct level of detail for a given view. pub lod_group_sphere: MeshletBoundingSphere, - /// Bounding sphere used for determining if this meshlet's parent group is at the correct level of detail for a given view. - pub lod_parent_group_sphere: MeshletBoundingSphere, +} + +/// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabb { + pub center: Vec3, + pub half_extent: Vec3, +} + +// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabbErrorOffset { + pub center: Vec3, + pub error: f32, + pub half_extent: Vec3, + pub child_offset: u32, } /// A spherical bounding volume used for a [`Meshlet`]. -#[derive(Copy, Clone, Pod, Zeroable)] +#[derive(Copy, Clone, Default, Pod, Zeroable)] #[repr(C)] pub struct MeshletBoundingSphere { pub center: Vec3, pub radius: f32, } -/// Simplification error used for choosing level of detail for a [`Meshlet`]. -#[derive(Copy, Clone, Pod, Zeroable)] -#[repr(C)] -pub struct MeshletSimplificationError { - /// Simplification error used for determining if this meshlet's group is at the correct level of detail for a given view. - pub group_error: f16, - /// Simplification error used for determining if this meshlet's parent group is at the correct level of detail for a given view. - pub parent_group_error: f16, -} - /// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. pub struct MeshletMeshSaver; @@ -143,15 +169,23 @@ impl AssetSaver for MeshletMeshSaver { .write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes()) .await?; + writer.write_all(bytemuck::bytes_of(&asset.aabb)).await?; + writer + .write_all(bytemuck::bytes_of(&asset.bvh_depth)) + .await?; + // Compress and write asset data let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer)); write_slice(&asset.vertex_positions, &mut writer)?; write_slice(&asset.vertex_normals, &mut writer)?; write_slice(&asset.vertex_uvs, &mut writer)?; write_slice(&asset.indices, &mut writer)?; + write_slice(&asset.bvh, &mut writer)?; write_slice(&asset.meshlets, &mut writer)?; - write_slice(&asset.meshlet_bounding_spheres, &mut writer)?; - write_slice(&asset.meshlet_simplification_errors, &mut writer)?; + write_slice(&asset.meshlet_cull_data, &mut writer)?; + // BUG: Flushing helps with an async_fs bug, but it still fails sometimes. https://github.com/smol-rs/async-fs/issues/45 + // ERROR bevy_asset::server: Failed to load asset with asset loader MeshletMeshLoader: failed to fill whole buffer + writer.flush()?; writer.finish()?; Ok(()) @@ -184,24 +218,33 @@ impl AssetLoader for MeshletMeshLoader { return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version }); } + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let aabb = bytemuck::cast(bytes); + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let bvh_depth = u32::from_le_bytes(bytes); + // Load and decompress asset data let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader)); let vertex_positions = read_slice(reader)?; let vertex_normals = read_slice(reader)?; let vertex_uvs = read_slice(reader)?; let indices = read_slice(reader)?; + let bvh = read_slice(reader)?; let meshlets = read_slice(reader)?; - let meshlet_bounding_spheres = read_slice(reader)?; - let meshlet_simplification_errors = read_slice(reader)?; + let meshlet_cull_data = read_slice(reader)?; Ok(MeshletMesh { vertex_positions, vertex_normals, vertex_uvs, indices, + bvh, meshlets, - meshlet_bounding_spheres, - meshlet_simplification_errors, + meshlet_cull_data, + aabb, + bvh_depth, }) } @@ -218,7 +261,7 @@ pub enum MeshletMeshSaveOrLoadError { WrongVersion { found: u64 }, #[error("failed to compress or decompress asset data")] CompressionOrDecompression(#[from] lz4_flex::frame::Error), - #[error("failed to read or write asset data")] + #[error(transparent)] Io(#[from] std::io::Error), } diff --git a/crates/bevy_pbr/src/meshlet/cull_bvh.wgsl b/crates/bevy_pbr/src/meshlet/cull_bvh.wgsl new file mode 100644 index 0000000000..b0bbb5f89b --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/cull_bvh.wgsl @@ -0,0 +1,110 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + get_aabb, + get_aabb_error, + get_aabb_child_offset, + constants, + meshlet_bvh_nodes, + meshlet_bvh_cull_count_read, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_meshlet_cull_count_early, + meshlet_meshlet_cull_count_late, + meshlet_meshlet_cull_dispatch_early, + meshlet_meshlet_cull_dispatch_late, + meshlet_meshlet_cull_queue, + meshlet_second_pass_bvh_count, + meshlet_second_pass_bvh_dispatch, + meshlet_second_pass_bvh_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +@compute +@workgroup_size(128, 1, 1) // 8 threads per node, 16 nodes per workgroup +fn cull_bvh(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the queue ID for this thread + let dispatch_id = global_invocation_id.x; + var node = dispatch_id >> 3u; + let subnode = dispatch_id & 7u; + if node >= meshlet_bvh_cull_count_read { return; } + + node = select(node, constants.rightmost_slot - node, constants.read_from_front == 0u); + let instanced_offset = meshlet_bvh_cull_queue[node]; + let instance_id = instanced_offset.instance_id; + let bvh_node = &meshlet_bvh_nodes[instanced_offset.offset]; + + var aabb_error_offset = (*bvh_node).aabbs[subnode]; + let aabb = get_aabb(&aabb_error_offset); + let parent_error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*bvh_node).lod_bounds[subnode]; + + let parent_is_imperceptible = lod_error_is_imperceptible(lod_sphere, parent_error, instance_id); + // Error and frustum cull, in both passes + if parent_is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } + + let child_offset = get_aabb_child_offset(&aabb_error_offset); + let index = subnode >> 2u; + let bit_offset = subnode & 3u; + let packed_child_count = (*bvh_node).child_counts[index]; + let child_count = extractBits(packed_child_count, bit_offset * 8u, 8u); + var value = InstancedOffset(instance_id, child_offset); + + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + if child_count == 255u { + let id = atomicAdd(&meshlet_second_pass_bvh_count, 1u); + meshlet_second_pass_bvh_queue[id] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_second_pass_bvh_dispatch.x, 1u); + } + } else { + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); + } +#endif + return; + } + + // If we pass, push the children to the next BVH cull + if child_count == 255u { + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + let index = select(constants.rightmost_slot - id, id, constants.read_from_front == 0u); + meshlet_bvh_cull_queue[index] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } + } else { +#ifdef MESHLET_FIRST_CULLING_PASS + let base = atomicAdd(&meshlet_meshlet_cull_count_early, child_count); + let end = base + child_count; + for (var i = base; i < end; i++) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (end + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_early.x, req); +#else + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); +#endif + } +} diff --git a/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl b/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl index 47f6dbb04b..85cbc0654d 100644 --- a/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl +++ b/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl @@ -1,194 +1,93 @@ #import bevy_pbr::meshlet_bindings::{ - meshlet_cluster_meshlet_ids, - meshlet_bounding_spheres, - meshlet_simplification_errors, - meshlet_cluster_instance_ids, - meshlet_instance_uniforms, - meshlet_second_pass_candidates, - depth_pyramid, + InstancedOffset, + get_aabb, + get_aabb_error, + constants, view, - previous_view, - should_cull_instance, - cluster_is_second_pass_candidate, + meshlet_instance_uniforms, + meshlet_cull_data, meshlet_software_raster_indirect_args, meshlet_hardware_raster_indirect_args, + meshlet_previous_raster_counts, meshlet_raster_clusters, - constants, - MeshletBoundingSphere, + meshlet_meshlet_cull_count_read, + meshlet_meshlet_cull_count_write, + meshlet_meshlet_cull_dispatch, + meshlet_meshlet_cull_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + ScreenAabb, + project_aabb, + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, } #import bevy_render::maths::affine3_to_square -/// Culls individual clusters (1 per thread) in two passes (two pass occlusion culling), and outputs a bitmask of which clusters survived. -/// 1. The first pass tests instance visibility, frustum culling, LOD selection, and finally occlusion culling using last frame's depth pyramid. -/// 2. The second pass performs occlusion culling (using the depth buffer generated from the first pass) on all clusters that passed -/// the instance, frustum, and LOD tests in the first pass, but were not visible last frame according to the occlusion culling. - @compute -@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 cluster per thread -fn cull_clusters( - @builtin(workgroup_id) workgroup_id: vec3, - @builtin(num_workgroups) num_workgroups: vec3, - @builtin(local_invocation_index) local_invocation_index: u32, -) { - // Calculate the cluster ID for this thread - let cluster_id = local_invocation_index + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); - if cluster_id >= constants.scene_cluster_count { return; } - -#ifdef MESHLET_SECOND_CULLING_PASS - if !cluster_is_second_pass_candidate(cluster_id) { return; } -#endif - - // Check for instance culling - let instance_id = meshlet_cluster_instance_ids[cluster_id]; -#ifdef MESHLET_FIRST_CULLING_PASS - if should_cull_instance(instance_id) { return; } -#endif - - // Calculate world-space culling bounding sphere for the cluster - let instance_uniform = meshlet_instance_uniforms[instance_id]; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; - let world_from_local = affine3_to_square(instance_uniform.world_from_local); - let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2]))); - let bounding_spheres = meshlet_bounding_spheres[meshlet_id]; - let culling_bounding_sphere_center = world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0); - let culling_bounding_sphere_radius = world_scale * bounding_spheres.culling_sphere.radius; +@workgroup_size(128, 1, 1) // 1 cluster per thread +fn cull_clusters(@builtin(global_invocation_id) global_invocation_id: vec3) { + if global_invocation_id.x >= meshlet_meshlet_cull_count_read { return; } #ifdef MESHLET_FIRST_CULLING_PASS - // Frustum culling - // TODO: Faster method from https://vkguide.dev/docs/gpudriven/compute_culling/#frustum-culling-function - for (var i = 0u; i < 6u; i++) { - if dot(view.frustum[i], culling_bounding_sphere_center) + culling_bounding_sphere_radius <= 0.0 { - return; - } - } - - // Check LOD cut (cluster group error imperceptible, and parent group error not imperceptible) - let simplification_errors = unpack2x16float(meshlet_simplification_errors[meshlet_id]); - let lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_group_sphere, simplification_errors.x, world_from_local, world_scale); - let parent_lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_parent_group_sphere, simplification_errors.y, world_from_local, world_scale); - if !lod_is_ok || parent_lod_is_ok { return; } -#endif - - // Project the culling bounding sphere to view-space for occlusion culling -#ifdef MESHLET_FIRST_CULLING_PASS - let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local); - let previous_world_from_local_scale = max(length(previous_world_from_local[0]), max(length(previous_world_from_local[1]), length(previous_world_from_local[2]))); - let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0); - let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.culling_sphere.radius; - let occlusion_culling_bounding_sphere_center_view_space = (previous_view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz; + let meshlet_id = global_invocation_id.x; #else - let occlusion_culling_bounding_sphere_center = culling_bounding_sphere_center; - let occlusion_culling_bounding_sphere_radius = culling_bounding_sphere_radius; - let occlusion_culling_bounding_sphere_center_view_space = (view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz; + let meshlet_id = constants.rightmost_slot - global_invocation_id.x; #endif + let instanced_offset = meshlet_meshlet_cull_queue[meshlet_id]; + let instance_id = instanced_offset.instance_id; + let cull_data = &meshlet_cull_data[instanced_offset.offset]; + var aabb_error_offset = (*cull_data).aabb; + let aabb = get_aabb(&aabb_error_offset); + let error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*cull_data).lod_group_sphere; - var aabb = project_view_space_sphere_to_screen_space_aabb(occlusion_culling_bounding_sphere_center_view_space, occlusion_culling_bounding_sphere_radius); - let depth_pyramid_size_mip_0 = vec2(textureDimensions(depth_pyramid, 0)); - var aabb_width_pixels = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x; - var aabb_height_pixels = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y; - let depth_level = max(0, i32(ceil(log2(max(aabb_width_pixels, aabb_height_pixels))))); // TODO: Naga doesn't like this being a u32 - let depth_pyramid_size = vec2(textureDimensions(depth_pyramid, depth_level)); - let aabb_top_left = vec2(aabb.xy * depth_pyramid_size); + let is_imperceptible = lod_error_is_imperceptible(lod_sphere, error, instance_id); + // Error and frustum cull, in both passes + if !is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } - let depth_quad_a = textureLoad(depth_pyramid, aabb_top_left, depth_level).x; - let depth_quad_b = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 0u), depth_level).x; - let depth_quad_c = textureLoad(depth_pyramid, aabb_top_left + vec2(0u, 1u), depth_level).x; - let depth_quad_d = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 1u), depth_level).x; - let occluder_depth = min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d)); - - // Check whether or not the cluster would be occluded if drawn - var cluster_visible: bool; - if view.clip_from_view[3][3] == 1.0 { - // Orthographic - let sphere_depth = view.clip_from_view[3][2] + (occlusion_culling_bounding_sphere_center_view_space.z + occlusion_culling_bounding_sphere_radius) * view.clip_from_view[2][2]; - cluster_visible = sphere_depth >= occluder_depth; - } else { - // Perspective - let sphere_depth = -view.clip_from_view[3][2] / (occlusion_culling_bounding_sphere_center_view_space.z + occlusion_culling_bounding_sphere_radius); - cluster_visible = sphere_depth >= occluder_depth; - } - - // Write if the cluster should be occlusion tested in the second pass + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { #ifdef MESHLET_FIRST_CULLING_PASS - if !cluster_visible { - let bit = 1u << cluster_id % 32u; - atomicOr(&meshlet_second_pass_candidates[cluster_id / 32u], bit); - } + let id = atomicAdd(&meshlet_meshlet_cull_count_write, 1u); + let value = InstancedOffset(instance_id, instanced_offset.offset); + meshlet_meshlet_cull_queue[constants.rightmost_slot - id] = value; + if ((id & 127u) == 0) { + atomicAdd(&meshlet_meshlet_cull_dispatch.x, 1u); + } #endif + return; + } - // Cluster would be occluded if drawn, so don't setup a draw for it - if !cluster_visible { return; } - + // If we pass, rasterize the meshlet // Check how big the cluster is in screen space -#ifdef MESHLET_FIRST_CULLING_PASS - let culling_bounding_sphere_center_view_space = (view.view_from_world * vec4(culling_bounding_sphere_center.xyz, 1.0)).xyz; - aabb = project_view_space_sphere_to_screen_space_aabb(culling_bounding_sphere_center_view_space, culling_bounding_sphere_radius); - aabb_width_pixels = (aabb.z - aabb.x) * view.viewport.z; - aabb_height_pixels = (aabb.w - aabb.y) * view.viewport.w; -#endif - let cluster_is_small = all(vec2(aabb_width_pixels, aabb_height_pixels) < vec2(64.0)); - - // Let the hardware rasterizer handle near-plane clipping - let not_intersects_near_plane = dot(view.frustum[4u], culling_bounding_sphere_center) > culling_bounding_sphere_radius; + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let projection = view.clip_from_world; + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + var sw_raster = project_aabb(clip_from_local, near, aabb, &screen_aabb); + if sw_raster { + let aabb_size = (screen_aabb.max.xy - screen_aabb.min.xy) * view.viewport.zw; + sw_raster = all(aabb_size <= vec2(64.0)); + } var buffer_slot: u32; - if cluster_is_small && not_intersects_near_plane { + if sw_raster { // Append this cluster to the list for software rasterization buffer_slot = atomicAdd(&meshlet_software_raster_indirect_args.x, 1u); + buffer_slot += meshlet_previous_raster_counts[0]; } else { // Append this cluster to the list for hardware rasterization buffer_slot = atomicAdd(&meshlet_hardware_raster_indirect_args.instance_count, 1u); - buffer_slot = constants.meshlet_raster_cluster_rightmost_slot - buffer_slot; - } - meshlet_raster_clusters[buffer_slot] = cluster_id; -} - -// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115 -fn lod_error_is_imperceptible(lod_sphere: MeshletBoundingSphere, simplification_error: f32, world_from_local: mat4x4, world_scale: f32) -> bool { - let sphere_world_space = (world_from_local * vec4(lod_sphere.center, 1.0)).xyz; - let radius_world_space = world_scale * lod_sphere.radius; - let error_world_space = world_scale * simplification_error; - - var projected_error = error_world_space; - if view.clip_from_view[3][3] != 1.0 { - // Perspective - let distance_to_closest_point_on_sphere = distance(sphere_world_space, view.world_position) - radius_world_space; - let distance_to_closest_point_on_sphere_clamped_to_znear = max(distance_to_closest_point_on_sphere, view.clip_from_view[3][2]); - projected_error /= distance_to_closest_point_on_sphere_clamped_to_znear; - } - projected_error *= view.clip_from_view[1][1] * 0.5; - projected_error *= view.viewport.w; - - return projected_error < 1.0; -} - -// https://zeux.io/2023/01/12/approximate-projected-bounds -fn project_view_space_sphere_to_screen_space_aabb(cp: vec3, r: f32) -> vec4 { - let inv_width = view.clip_from_view[0][0] * 0.5; - let inv_height = view.clip_from_view[1][1] * 0.5; - if view.clip_from_view[3][3] == 1.0 { - // Orthographic - let min_x = cp.x - r; - let max_x = cp.x + r; - - let min_y = cp.y - r; - let max_y = cp.y + r; - - return vec4(min_x * inv_width, 1.0 - max_y * inv_height, max_x * inv_width, 1.0 - min_y * inv_height); - } else { - // Perspective - let c = vec3(cp.xy, -cp.z); - let cr = c * r; - let czr2 = c.z * c.z - r * r; - - let vx = sqrt(c.x * c.x + czr2); - let min_x = (vx * c.x - cr.z) / (vx * c.z + cr.x); - let max_x = (vx * c.x + cr.z) / (vx * c.z - cr.x); - - let vy = sqrt(c.y * c.y + czr2); - let min_y = (vy * c.y - cr.z) / (vy * c.z + cr.y); - let max_y = (vy * c.y + cr.z) / (vy * c.z - cr.y); - - return vec4(min_x * inv_width, -max_y * inv_height, max_x * inv_width, -min_y * inv_height) + vec4(0.5); + buffer_slot += meshlet_previous_raster_counts[1]; + buffer_slot = constants.rightmost_slot - buffer_slot; } + meshlet_raster_clusters[buffer_slot] = InstancedOffset(instance_id, instanced_offset.offset); } diff --git a/crates/bevy_pbr/src/meshlet/cull_instances.wgsl b/crates/bevy_pbr/src/meshlet/cull_instances.wgsl new file mode 100644 index 0000000000..5d14d10b6f --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/cull_instances.wgsl @@ -0,0 +1,76 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + constants, + meshlet_view_instance_visibility, + meshlet_instance_aabbs, + meshlet_instance_bvh_root_nodes, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_second_pass_instance_count, + meshlet_second_pass_instance_dispatch, + meshlet_second_pass_instance_candidates, +} +#import bevy_pbr::meshlet_cull_shared::{ + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +fn instance_count() -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return constants.scene_instance_count; +#else + return meshlet_second_pass_instance_count; +#endif +} + +fn map_instance_id(id: u32) -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return id; +#else + return meshlet_second_pass_instance_candidates[id]; +#endif +} + +fn should_cull_instance(instance_id: u32) -> bool { + let bit_offset = instance_id >> 5u; + let packed_visibility = meshlet_view_instance_visibility[instance_id & 31u]; + return bool(extractBits(packed_visibility, bit_offset, 1u)); +} + +@compute +@workgroup_size(128, 1, 1) // 1 instance per thread +fn cull_instances(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the instance ID for this thread + let dispatch_id = global_invocation_id.x; + if dispatch_id >= instance_count() { return; } + + let instance_id = map_instance_id(dispatch_id); + let aabb = meshlet_instance_aabbs[instance_id]; + + // Visibility and frustum cull, but only in the first pass +#ifdef MESHLET_FIRST_CULLING_PASS + if should_cull_instance(instance_id) || !aabb_in_frustum(aabb, instance_id) { return; } +#endif + + // If we pass, try occlusion culling + // If this instance was occluded, push it to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + let id = atomicAdd(&meshlet_second_pass_instance_count, 1u); + meshlet_second_pass_instance_candidates[id] = instance_id; + if ((id & 127u) == 0u) { + atomicAdd(&meshlet_second_pass_instance_dispatch.x, 1u); + } +#endif + return; + } + + // If we pass, push the instance's root node to BVH cull + let root_node = meshlet_instance_bvh_root_nodes[instance_id]; + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + meshlet_bvh_cull_queue[id] = InstancedOffset(instance_id, root_node); + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } +} diff --git a/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl b/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl deleted file mode 100644 index db39ae2bce..0000000000 --- a/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl +++ /dev/null @@ -1,50 +0,0 @@ -#import bevy_pbr::meshlet_bindings::{ - scene_instance_count, - meshlet_global_cluster_count, - meshlet_instance_meshlet_counts, - meshlet_instance_meshlet_slice_starts, - meshlet_cluster_instance_ids, - meshlet_cluster_meshlet_ids, -} - -/// Writes out instance_id and meshlet_id to the global buffers for each cluster in the scene. - -var cluster_slice_start_workgroup: u32; - -@compute -@workgroup_size(1024, 1, 1) // 1024 threads per workgroup, 1 instance per workgroup -fn fill_cluster_buffers( - @builtin(workgroup_id) workgroup_id: vec3, - @builtin(num_workgroups) num_workgroups: vec3, - @builtin(local_invocation_index) local_invocation_index: u32, -) { - // Calculate the instance ID for this workgroup - var instance_id = workgroup_id.x + (workgroup_id.y * num_workgroups.x); - if instance_id >= scene_instance_count { return; } - - let instance_meshlet_count = meshlet_instance_meshlet_counts[instance_id]; - let instance_meshlet_slice_start = meshlet_instance_meshlet_slice_starts[instance_id]; - - // Reserve cluster slots for the instance and broadcast to the workgroup - if local_invocation_index == 0u { - cluster_slice_start_workgroup = atomicAdd(&meshlet_global_cluster_count, instance_meshlet_count); - } - let cluster_slice_start = workgroupUniformLoad(&cluster_slice_start_workgroup); - - // Loop enough times to write out all the meshlets for the instance given that each thread writes 1 meshlet in each iteration - for (var clusters_written = 0u; clusters_written < instance_meshlet_count; clusters_written += 1024u) { - // Calculate meshlet ID within this instance's MeshletMesh to process for this thread - let meshlet_id_local = clusters_written + local_invocation_index; - if meshlet_id_local >= instance_meshlet_count { return; } - - // Find the overall cluster ID in the global cluster buffer - let cluster_id = cluster_slice_start + meshlet_id_local; - - // Find the overall meshlet ID in the global meshlet buffer - let meshlet_id = instance_meshlet_slice_start + meshlet_id_local; - - // Write results to buffers - meshlet_cluster_instance_ids[cluster_id] = instance_id; - meshlet_cluster_meshlet_ids[cluster_id] = meshlet_id; - } -} diff --git a/crates/bevy_pbr/src/meshlet/fill_counts.wgsl b/crates/bevy_pbr/src/meshlet/fill_counts.wgsl new file mode 100644 index 0000000000..f319e395d9 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/fill_counts.wgsl @@ -0,0 +1,35 @@ +/// Copies the counts of meshlets in the hardware and software buckets, resetting the counters in the process. + +struct DispatchIndirectArgs { + x: u32, + y: u32, + z: u32, +} + +struct DrawIndirectArgs { + vertex_count: u32, + instance_count: u32, + first_vertex: u32, + first_instance: u32, +} + +@group(0) @binding(0) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(1) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(2) var meshlet_previous_raster_counts: array; +#ifdef MESHLET_2D_DISPATCH +@group(0) @binding(3) var meshlet_software_raster_cluster_count: u32; +#endif + +@compute +@workgroup_size(1, 1, 1) +fn fill_counts() { +#ifdef MESHLET_2D_DISPATCH + meshlet_previous_raster_counts[0] += meshlet_software_raster_cluster_count; +#else + meshlet_previous_raster_counts[0] += meshlet_software_raster_indirect_args.x; +#endif + meshlet_software_raster_indirect_args.x = 0; + + meshlet_previous_raster_counts[1] += meshlet_hardware_raster_indirect_args.instance_count; + meshlet_hardware_raster_indirect_args.instance_count = 0; +} diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index ed2be52f53..db8b8a96cf 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -1,16 +1,20 @@ -use super::asset::{ - Meshlet, MeshletBoundingSphere, MeshletBoundingSpheres, MeshletMesh, MeshletSimplificationError, -}; +use crate::meshlet::asset::{MeshletAabb, MeshletAabbErrorOffset, MeshletCullData}; + +use super::asset::{BvhNode, Meshlet, MeshletBoundingSphere, MeshletMesh}; use alloc::borrow::Cow; -use bevy_math::{ops::log2, IVec3, Vec2, Vec3, Vec3Swizzles}; +use bevy_math::{ + bounding::{Aabb3d, BoundingSphere, BoundingVolume}, + ops::log2, + IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles, +}; use bevy_platform::collections::HashMap; use bevy_render::{ mesh::{Indices, Mesh}, render_resource::PrimitiveTopology, }; +use bevy_tasks::{AsyncComputeTaskPool, ParallelSlice}; use bitvec::{order::Lsb0, vec::BitVec, view::BitView}; -use core::{iter, ops::Range}; -use half::f16; +use core::{f32, ops::Range}; use itertools::Itertools; use meshopt::{ build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi, @@ -19,11 +23,13 @@ use meshopt::{ use metis::{option::Opt, Graph}; use smallvec::SmallVec; use thiserror::Error; +use tracing::debug_span; // Aim to have 8 meshlets per group const TARGET_MESHLETS_PER_GROUP: usize = 8; -// Reject groups that keep over 95% of their original triangles -const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.95; +// Reject groups that keep over 60% of their original triangles. We'd much rather render a few +// extra triangles than create too many meshlets, increasing cull overhead. +const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.60; /// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`]. /// @@ -64,6 +70,9 @@ impl MeshletMesh { mesh: &Mesh, vertex_position_quantization_factor: u8, ) -> Result { + let s = debug_span!("build meshlet mesh"); + let _e = s.enter(); + // Validate mesh format let indices = validate_input_mesh(mesh)?; @@ -84,41 +93,28 @@ impl MeshletMesh { ); // Split the mesh into an initial list of meshlets (LOD 0) - let mut meshlets = compute_meshlets( + let (mut meshlets, mut cull_data) = compute_meshlets( &indices, &vertices, &position_only_vertex_remap, position_only_vertex_count, + None, ); - let mut bounding_spheres = meshlets - .iter() - .map(|meshlet| compute_meshlet_bounds(meshlet, &vertices)) - .map(|bounding_sphere| MeshletBoundingSpheres { - culling_sphere: bounding_sphere, - lod_group_sphere: bounding_sphere, - lod_parent_group_sphere: MeshletBoundingSphere { - center: Vec3::ZERO, - radius: 0.0, - }, - }) - .collect::>(); - let mut simplification_errors = iter::repeat_n( - MeshletSimplificationError { - group_error: f16::ZERO, - parent_group_error: f16::MAX, - }, - meshlets.len(), - ) - .collect::>(); let mut vertex_locks = vec![false; vertices.vertex_count]; // Build further LODs - let mut simplification_queue = 0..meshlets.len(); - while simplification_queue.len() > 1 { + let mut bvh = BvhBuilder::default(); + let mut all_groups = Vec::new(); + let mut simplification_queue: Vec<_> = (0..meshlets.len() as u32).collect(); + let mut stuck = Vec::new(); + while !simplification_queue.is_empty() { + let s = debug_span!("simplify lod", meshlets = simplification_queue.len()); + let _e = s.enter(); + // For each meshlet build a list of connected meshlets (meshlets that share a vertex) let connected_meshlets_per_meshlet = find_connected_meshlets( - simplification_queue.clone(), + &simplification_queue, &meshlets, &position_only_vertex_remap, position_only_vertex_count, @@ -127,9 +123,11 @@ impl MeshletMesh { // Group meshlets into roughly groups of size TARGET_MESHLETS_PER_GROUP, // grouping meshlets with a high number of shared vertices let groups = group_meshlets( + &simplification_queue, + &cull_data, &connected_meshlets_per_meshlet, - simplification_queue.clone(), ); + simplification_queue.clear(); // Lock borders between groups to prevent cracks when simplifying lock_group_borders( @@ -140,16 +138,20 @@ impl MeshletMesh { position_only_vertex_count, ); - let next_lod_start = meshlets.len(); - for group_meshlets in groups.into_iter() { + let simplified = groups.par_chunk_map(AsyncComputeTaskPool::get(), 1, |_, groups| { + let mut group = groups[0].clone(); + // If the group only has a single meshlet we can't simplify it - if group_meshlets.len() == 1 { - continue; + if group.meshlets.len() == 1 { + return Err(group); } + let s = debug_span!("simplify group", meshlets = group.meshlets.len()); + let _e = s.enter(); + // Simplify the group to ~50% triangle count let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group( - &group_meshlets, + &group, &meshlets, &vertices, vertex_normals, @@ -157,51 +159,70 @@ impl MeshletMesh { &vertex_locks, ) else { // Couldn't simplify the group enough - continue; + return Err(group); }; - // Compute LOD data for the group - let group_bounding_sphere = compute_lod_group_data( - &group_meshlets, - &mut group_error, - &mut bounding_spheres, - &mut simplification_errors, - ); + // Force the group error to be atleast as large as all of its constituent meshlet's + // individual errors. + for &id in group.meshlets.iter() { + group_error = group_error.max(cull_data[id as usize].error); + } + group.parent_error = group_error; // Build new meshlets using the simplified group - let new_meshlets_count = split_simplified_group_into_new_meshlets( + let new_meshlets = compute_meshlets( &simplified_group_indices, &vertices, &position_only_vertex_remap, position_only_vertex_count, - &mut meshlets, + Some((group.lod_bounds, group.parent_error)), ); - // Calculate the culling bounding sphere for the new meshlets and set their LOD group data - let new_meshlet_ids = (meshlets.len() - new_meshlets_count)..meshlets.len(); - bounding_spheres.extend(new_meshlet_ids.clone().map(|meshlet_id| { - MeshletBoundingSpheres { - culling_sphere: compute_meshlet_bounds(meshlets.get(meshlet_id), &vertices), - lod_group_sphere: group_bounding_sphere, - lod_parent_group_sphere: MeshletBoundingSphere { - center: Vec3::ZERO, - radius: 0.0, - }, + Ok((group, new_meshlets)) + }); + + let first_group = all_groups.len() as u32; + let mut passed_tris = 0; + let mut stuck_tris = 0; + for group in simplified { + match group { + Ok((group, (new_meshlets, new_cull_data))) => { + let start = meshlets.len(); + merge_meshlets(&mut meshlets, new_meshlets); + cull_data.extend(new_cull_data); + let end = meshlets.len(); + let new_meshlet_ids = start as u32..end as u32; + + passed_tris += triangles_in_meshlets(&meshlets, new_meshlet_ids.clone()); + simplification_queue.extend(new_meshlet_ids); + all_groups.push(group); } - })); - simplification_errors.extend(iter::repeat_n( - MeshletSimplificationError { - group_error, - parent_group_error: f16::MAX, - }, - new_meshlet_ids.len(), - )); + Err(group) => { + stuck_tris += + triangles_in_meshlets(&meshlets, group.meshlets.iter().copied()); + stuck.push(group); + } + } } - // Set simplification queue to the list of newly created meshlets - simplification_queue = next_lod_start..meshlets.len(); + // If we have enough triangles that passed, we can retry simplifying the stuck + // meshlets. + if passed_tris > stuck_tris / 3 { + simplification_queue.extend(stuck.drain(..).flat_map(|group| group.meshlets)); + } + + bvh.add_lod(first_group, &all_groups); } + // If there's any stuck meshlets left, add another LOD level with only them + if !stuck.is_empty() { + let first_group = all_groups.len() as u32; + all_groups.extend(stuck); + bvh.add_lod(first_group, &all_groups); + } + + let (bvh, aabb, depth) = bvh.build(&mut meshlets, all_groups, &mut cull_data); + // Copy vertex attributes per meshlet and compress let mut vertex_positions = BitVec::::new(); let mut vertex_normals = Vec::new(); @@ -227,9 +248,17 @@ impl MeshletMesh { vertex_normals: vertex_normals.into(), vertex_uvs: vertex_uvs.into(), indices: meshlets.triangles.into(), + bvh: bvh.into(), meshlets: bevy_meshlets.into(), - meshlet_bounding_spheres: bounding_spheres.into(), - meshlet_simplification_errors: simplification_errors.into(), + meshlet_cull_data: cull_data + .into_iter() + .map(|cull_data| MeshletCullData { + aabb: aabb_to_meshlet(cull_data.aabb, cull_data.error, 0), + lod_group_sphere: sphere_to_meshlet(cull_data.lod_group_sphere), + }) + .collect(), + aabb, + bvh_depth: depth, }) } } @@ -244,7 +273,11 @@ fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshC Mesh::ATTRIBUTE_NORMAL.id, Mesh::ATTRIBUTE_UV_0.id, ]) { - return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes); + return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes( + mesh.attributes() + .map(|(attribute, _)| format!("{attribute:?}")) + .collect(), + )); } match mesh.indices() { @@ -254,12 +287,19 @@ fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshC } } +fn triangles_in_meshlets(meshlets: &Meshlets, ids: impl IntoIterator) -> u32 { + ids.into_iter() + .map(|id| meshlets.get(id as _).triangles.len() as u32 / 3) + .sum() +} + fn compute_meshlets( indices: &[u32], vertices: &VertexDataAdapter, position_only_vertex_remap: &[u32], position_only_vertex_count: usize, -) -> Meshlets { + prev_lod_data: Option<(BoundingSphere, f32)>, +) -> (Meshlets, Vec) { // For each vertex, build a list of all triangles that use it let mut vertices_to_triangles = vec![Vec::new(); position_only_vertex_count]; for (i, index) in indices.iter().enumerate() { @@ -293,6 +333,7 @@ fn compute_meshlets( } // The order of triangles depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? for list in connected_triangles_per_triangle.iter_mut() { list.sort_unstable(); } @@ -336,40 +377,52 @@ fn compute_meshlets( vertices: Vec::new(), triangles: Vec::new(), }; + let mut cull_data = Vec::new(); + let get_vertex = |&v: &u32| { + *bytemuck::from_bytes::( + &vertices.reader.get_ref() + [vertices.position_offset + v as usize * vertices.vertex_stride..][..12], + ) + }; for meshlet_indices in &indices_per_meshlet { let meshlet = build_meshlets(meshlet_indices, vertices, 255, 128, 0.0); - let vertex_offset = meshlets.vertices.len() as u32; - let triangle_offset = meshlets.triangles.len() as u32; - meshlets.vertices.extend_from_slice(&meshlet.vertices); - meshlets.triangles.extend_from_slice(&meshlet.triangles); - meshlets - .meshlets - .extend(meshlet.meshlets.into_iter().map(|mut meshlet| { - meshlet.vertex_offset += vertex_offset; - meshlet.triangle_offset += triangle_offset; - meshlet - })); + for meshlet in meshlet.iter() { + let (lod_group_sphere, error) = prev_lod_data.unwrap_or_else(|| { + let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices); + (BoundingSphere::new(bounds.center, bounds.radius), 0.0) + }); + + cull_data.push(TempMeshletCullData { + aabb: Aabb3d::from_point_cloud( + Isometry3d::IDENTITY, + meshlet.vertices.iter().map(get_vertex), + ), + lod_group_sphere, + error, + }); + } + merge_meshlets(&mut meshlets, meshlet); } - meshlets + (meshlets, cull_data) } fn find_connected_meshlets( - simplification_queue: Range, + simplification_queue: &[u32], meshlets: &Meshlets, position_only_vertex_remap: &[u32], position_only_vertex_count: usize, ) -> Vec> { // For each vertex, build a list of all meshlets that use it let mut vertices_to_meshlets = vec![Vec::new(); position_only_vertex_count]; - for meshlet_id in simplification_queue.clone() { - let meshlet = meshlets.get(meshlet_id); + for (id_index, &meshlet_id) in simplification_queue.iter().enumerate() { + let meshlet = meshlets.get(meshlet_id as _); for index in meshlet.triangles { let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize]; let vertex_to_meshlets = &mut vertices_to_meshlets[vertex_id as usize]; // Meshlets are added in order, so we can just check the last element to deduplicate, // in the case of two triangles sharing the same vertex within a single meshlet - if vertex_to_meshlets.last() != Some(&meshlet_id) { - vertex_to_meshlets.push(meshlet_id); + if vertex_to_meshlets.last() != Some(&id_index) { + vertex_to_meshlets.push(id_index); } } } @@ -389,13 +442,12 @@ fn find_connected_meshlets( let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()]; for ((meshlet_id1, meshlet_id2), shared_vertex_count) in meshlet_pair_to_shared_vertex_count { // We record both id1->id2 and id2->id1 as adjacency is symmetrical - connected_meshlets_per_meshlet[meshlet_id1 - simplification_queue.start] - .push((meshlet_id2, shared_vertex_count)); - connected_meshlets_per_meshlet[meshlet_id2 - simplification_queue.start] - .push((meshlet_id1, shared_vertex_count)); + connected_meshlets_per_meshlet[meshlet_id1].push((meshlet_id2, shared_vertex_count)); + connected_meshlets_per_meshlet[meshlet_id2].push((meshlet_id1, shared_vertex_count)); } // The order of meshlets depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? for list in connected_meshlets_per_meshlet.iter_mut() { list.sort_unstable(); } @@ -405,16 +457,17 @@ fn find_connected_meshlets( // METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf fn group_meshlets( + simplification_queue: &[u32], + meshlet_cull_data: &[TempMeshletCullData], connected_meshlets_per_meshlet: &[Vec<(usize, usize)>], - simplification_queue: Range, -) -> Vec> { +) -> Vec { let mut xadj = Vec::with_capacity(simplification_queue.len() + 1); let mut adjncy = Vec::new(); let mut adjwgt = Vec::new(); for connected_meshlets in connected_meshlets_per_meshlet { xadj.push(adjncy.len() as i32); for (connected_meshlet_id, shared_vertex_count) in connected_meshlets { - adjncy.push((connected_meshlet_id - simplification_queue.start) as i32); + adjncy.push(*connected_meshlet_id as i32); adjwgt.push(*shared_vertex_count as i32); // TODO: Additional weight based on meshlet spatial proximity } @@ -436,16 +489,22 @@ fn group_meshlets( .part_recursive(&mut group_per_meshlet) .unwrap(); - let mut groups = vec![SmallVec::new(); partition_count]; + let mut groups = vec![TempMeshletGroup::default(); partition_count]; for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() { - groups[meshlet_group as usize].push(i + simplification_queue.start); + let group = &mut groups[meshlet_group as usize]; + let meshlet_id = simplification_queue[i]; + + group.meshlets.push(meshlet_id); + let data = &meshlet_cull_data[meshlet_id as usize]; + group.aabb = group.aabb.merge(&data.aabb); + group.lod_bounds = merge_spheres(group.lod_bounds, data.lod_group_sphere); } groups } fn lock_group_borders( vertex_locks: &mut [bool], - groups: &[SmallVec<[usize; TARGET_MESHLETS_PER_GROUP]>], + groups: &[TempMeshletGroup], meshlets: &Meshlets, position_only_vertex_remap: &[u32], position_only_vertex_count: usize, @@ -453,9 +512,9 @@ fn lock_group_borders( let mut position_only_locks = vec![-1; position_only_vertex_count]; // Iterate over position-only based vertices of all meshlets in all groups - for (group_id, group_meshlets) in groups.iter().enumerate() { - for meshlet_id in group_meshlets { - let meshlet = meshlets.get(*meshlet_id); + for (group_id, group) in groups.iter().enumerate() { + for &meshlet_id in group.meshlets.iter() { + let meshlet = meshlets.get(meshlet_id as usize); for index in meshlet.triangles { let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize] as usize; @@ -480,21 +539,25 @@ fn lock_group_borders( } fn simplify_meshlet_group( - group_meshlets: &[usize], + group: &TempMeshletGroup, meshlets: &Meshlets, vertices: &VertexDataAdapter<'_>, vertex_normals: &[f32], vertex_stride: usize, vertex_locks: &[bool], -) -> Option<(Vec, f16)> { +) -> Option<(Vec, f32)> { // Build a new index buffer into the mesh vertex data by combining all meshlet data in the group - let mut group_indices = Vec::new(); - for meshlet_id in group_meshlets { - let meshlet = meshlets.get(*meshlet_id); - for meshlet_index in meshlet.triangles { - group_indices.push(meshlet.vertices[*meshlet_index as usize]); - } - } + let group_indices = group + .meshlets + .iter() + .flat_map(|&meshlet_id| { + let meshlet = meshlets.get(meshlet_id as _); + meshlet + .triangles + .iter() + .map(|&meshlet_index| meshlet.vertices[meshlet_index as usize]) + }) + .collect::>(); // Simplify the group to ~50% triangle count let mut error = 0.0; @@ -511,96 +574,28 @@ fn simplify_meshlet_group( Some(&mut error), ); - // Check if we were able to simplify at least a little + // Check if we were able to simplify if simplified_group_indices.len() as f32 / group_indices.len() as f32 > SIMPLIFICATION_FAILURE_PERCENTAGE { return None; } - Some((simplified_group_indices, f16::from_f32(error))) + Some((simplified_group_indices, error)) } -fn compute_lod_group_data( - group_meshlets: &[usize], - group_error: &mut f16, - bounding_spheres: &mut [MeshletBoundingSpheres], - simplification_errors: &mut [MeshletSimplificationError], -) -> MeshletBoundingSphere { - let mut group_bounding_sphere = MeshletBoundingSphere { - center: Vec3::ZERO, - radius: 0.0, - }; - - // Compute the lod group sphere center as a weighted average of the children spheres - let mut weight = 0.0; - for meshlet_id in group_meshlets { - let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere; - group_bounding_sphere.center += - meshlet_lod_bounding_sphere.center * meshlet_lod_bounding_sphere.radius; - weight += meshlet_lod_bounding_sphere.radius; - } - group_bounding_sphere.center /= weight; - - // Force parent group sphere to contain all child group spheres (we're currently building the parent from its children) - // TODO: This does not produce the absolute minimal bounding sphere. Doing so is non-trivial. - // "Smallest enclosing balls of balls" http://www.inf.ethz.ch/personal/emo/DoctThesisFiles/fischer05.pdf - for meshlet_id in group_meshlets { - let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere; - let d = meshlet_lod_bounding_sphere - .center - .distance(group_bounding_sphere.center); - group_bounding_sphere.radius = group_bounding_sphere - .radius - .max(meshlet_lod_bounding_sphere.radius + d); - } - - // Force parent error to be >= child error (we're currently building the parent from its children) - for meshlet_id in group_meshlets { - *group_error = group_error.max(simplification_errors[*meshlet_id].group_error); - } - - // Set the children's lod parent group data to the new lod group we just made - for meshlet_id in group_meshlets { - bounding_spheres[*meshlet_id].lod_parent_group_sphere = group_bounding_sphere; - simplification_errors[*meshlet_id].parent_group_error = *group_error; - } - - group_bounding_sphere -} - -fn split_simplified_group_into_new_meshlets( - simplified_group_indices: &[u32], - vertices: &VertexDataAdapter<'_>, - position_only_vertex_remap: &[u32], - position_only_vertex_count: usize, - meshlets: &mut Meshlets, -) -> usize { - let simplified_meshlets = compute_meshlets( - simplified_group_indices, - vertices, - position_only_vertex_remap, - position_only_vertex_count, - ); - let new_meshlets_count = simplified_meshlets.len(); - +fn merge_meshlets(meshlets: &mut Meshlets, merge: Meshlets) { let vertex_offset = meshlets.vertices.len() as u32; let triangle_offset = meshlets.triangles.len() as u32; - meshlets - .vertices - .extend_from_slice(&simplified_meshlets.vertices); - meshlets - .triangles - .extend_from_slice(&simplified_meshlets.triangles); + meshlets.vertices.extend_from_slice(&merge.vertices); + meshlets.triangles.extend_from_slice(&merge.triangles); meshlets .meshlets - .extend(simplified_meshlets.meshlets.into_iter().map(|mut meshlet| { + .extend(merge.meshlets.into_iter().map(|mut meshlet| { meshlet.vertex_offset += vertex_offset; meshlet.triangle_offset += triangle_offset; meshlet })); - - new_meshlets_count } fn build_and_compress_per_meshlet_vertex_data( @@ -688,14 +683,397 @@ fn build_and_compress_per_meshlet_vertex_data( }); } -fn compute_meshlet_bounds( - meshlet: meshopt::Meshlet<'_>, - vertices: &VertexDataAdapter<'_>, -) -> MeshletBoundingSphere { - let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices); +fn merge_spheres(a: BoundingSphere, b: BoundingSphere) -> BoundingSphere { + let sr = a.radius().min(b.radius()); + let br = a.radius().max(b.radius()); + let len = a.center.distance(b.center); + if len + sr <= br || sr == 0.0 || len == 0.0 { + if a.radius() > b.radius() { + a + } else { + b + } + } else { + let radius = (sr + br + len) / 2.0; + let center = + (a.center + b.center + (a.radius() - b.radius()) * (a.center - b.center) / len) / 2.0; + BoundingSphere::new(center, radius) + } +} + +#[derive(Copy, Clone)] +struct TempMeshletCullData { + aabb: Aabb3d, + lod_group_sphere: BoundingSphere, + error: f32, +} + +#[derive(Clone)] +struct TempMeshletGroup { + aabb: Aabb3d, + lod_bounds: BoundingSphere, + parent_error: f32, + meshlets: SmallVec<[u32; TARGET_MESHLETS_PER_GROUP]>, +} + +impl Default for TempMeshletGroup { + fn default() -> Self { + Self { + aabb: aabb_default(), // Default AABB to merge into + lod_bounds: BoundingSphere::new(Vec3A::ZERO, 0.0), + parent_error: f32::MAX, + meshlets: SmallVec::new(), + } + } +} + +// All the BVH build code was stolen from https://github.com/SparkyPotato/radiance/blob/4aa17a3a5be7a0466dc69713e249bbcee9f46057/crates/rad-renderer/src/assets/mesh/virtual_mesh.rs because it works and I'm lazy and don't want to reimplement it +struct TempBvhNode { + group: u32, + aabb: Aabb3d, + children: SmallVec<[u32; 8]>, +} + +#[derive(Default)] +struct BvhBuilder { + nodes: Vec, + lods: Vec>, +} + +impl BvhBuilder { + fn add_lod(&mut self, offset: u32, all_groups: &[TempMeshletGroup]) { + let first = self.nodes.len() as u32; + self.nodes.extend( + all_groups + .iter() + .enumerate() + .skip(offset as _) + .map(|(i, group)| TempBvhNode { + group: i as u32, + aabb: group.aabb, + children: SmallVec::new(), + }), + ); + let end = self.nodes.len() as u32; + if first != end { + self.lods.push(first..end); + } + } + + fn surface_area(&self, nodes: &[u32]) -> f32 { + nodes + .iter() + .map(|&x| self.nodes[x as usize].aabb) + .reduce(|a, b| a.merge(&b)) + .expect("cannot find surface area of zero nodes") + .visible_area() + } + + fn sort_nodes_by_sah(&self, nodes: &mut [u32], splits: [usize; 8]) { + // We use a BVH8, so just recursively binary split 3 times for near-optimal SAH + for i in 0..3 { + let parts = 1 << i; // 2^i + let nodes_per_split = 8 >> i; // 8 / 2^i + let half_count = nodes_per_split / 2; + let mut offset = 0; + for p in 0..parts { + let first = p * nodes_per_split; + let mut s0 = 0; + let mut s1 = 0; + for i in 0..half_count { + s0 += splits[first + i]; + s1 += splits[first + half_count + i]; + } + let c = s0 + s1; + let nodes = &mut nodes[offset..(offset + c)]; + offset += c; + + let mut cost = f32::MAX; + let mut axis = 0; + let key = |x, ax| self.nodes[x as usize].aabb.center()[ax]; + for ax in 0..3 { + nodes.sort_unstable_by(|&x, &y| key(x, ax).partial_cmp(&key(y, ax)).unwrap()); + let (left, right) = nodes.split_at(s0); + let c = self.surface_area(left) + self.surface_area(right); + if c < cost { + axis = ax; + cost = c; + } + } + if axis != 2 { + nodes.sort_unstable_by(|&x, &y| { + key(x, axis).partial_cmp(&key(y, axis)).unwrap() + }); + } + } + } + } + + fn build_temp_inner(&mut self, nodes: &mut [u32], optimize: bool) -> u32 { + let count = nodes.len(); + if count == 1 { + nodes[0] + } else if count <= 8 { + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children: nodes.iter().copied().collect(), + }); + i as _ + } else { + // We need to split the nodes into 8 groups, with the smallest possible tree depth. + // Additionally, no child should be more than one level deeper than the others. + // At `l` levels, we can fit upto 8^l nodes. + // The `max_child_size` is the largest power of 8 <= `count` (any larger and we'd have + // unfilled nodes). + // The `min_child_size` is thus 1 level (8 times) smaller. + // After distributing `min_child_size` to all children, we have distributed + // `min_child_size * 8` nodes (== `max_child_size`). + // The remaining nodes are then distributed left to right. + let max_child_size = 1 << ((count.ilog2() / 3) * 3); + let min_child_size = max_child_size >> 3; + let max_extra_per_node = max_child_size - min_child_size; + let mut extra = count - max_child_size; // 8 * min_child_size + let splits = core::array::from_fn(|_| { + let size = extra.min(max_extra_per_node); + extra -= size; + min_child_size + size + }); + + if optimize { + self.sort_nodes_by_sah(nodes, splits); + } + + let mut offset = 0; + let children = splits + .into_iter() + .map(|size| { + let i = self.build_temp_inner(&mut nodes[offset..(offset + size)], optimize); + offset += size; + i + }) + .collect(); + + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children, + }); + i as _ + } + } + + fn build_temp(&mut self) -> u32 { + let mut lods = Vec::with_capacity(self.lods.len()); + for lod in core::mem::take(&mut self.lods) { + let mut lod: Vec<_> = lod.collect(); + let root = self.build_temp_inner(&mut lod, true); + let node = &self.nodes[root as usize]; + if node.group != u32::MAX || node.children.len() == 8 { + lods.push(root); + } else { + lods.extend(node.children.iter().copied()); + } + } + self.build_temp_inner(&mut lods, false) + } + + fn build_inner( + &self, + groups: &[TempMeshletGroup], + out: &mut Vec, + max_depth: &mut u32, + node: u32, + depth: u32, + ) -> u32 { + *max_depth = depth.max(*max_depth); + let node = &self.nodes[node as usize]; + let onode = out.len(); + out.push(BvhNode::default()); + + for (i, &child_id) in node.children.iter().enumerate() { + let child = &self.nodes[child_id as usize]; + if child.group != u32::MAX { + let group = &groups[child.group as usize]; + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + out.lod_bounds[i] = sphere_to_meshlet(group.lod_bounds); + out.child_counts[i] = group.meshlets[1] as _; + } else { + let child_id = self.build_inner(groups, out, max_depth, child_id, depth + 1); + let child = &out[child_id as usize]; + let mut aabb = aabb_default(); + let mut parent_error = 0.0f32; + let mut lod_bounds = BoundingSphere::new(Vec3A::ZERO, 0.0); + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + child.aabbs[i].center, + child.aabbs[i].half_extent, + )); + lod_bounds = merge_spheres( + lod_bounds, + BoundingSphere::new(child.lod_bounds[i].center, child.lod_bounds[i].radius), + ); + parent_error = parent_error.max(child.aabbs[i].error); + } + + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(aabb, parent_error, child_id); + out.lod_bounds[i] = sphere_to_meshlet(lod_bounds); + out.child_counts[i] = u8::MAX; + } + } + + onode as _ + } + + fn build( + mut self, + meshlets: &mut Meshlets, + mut groups: Vec, + cull_data: &mut Vec, + ) -> (Vec, MeshletAabb, u32) { + // The BVH requires group meshlets to be contiguous, so remap them first. + let mut remap = Vec::with_capacity(meshlets.meshlets.len()); + let mut remapped_cull_data = Vec::with_capacity(cull_data.len()); + for group in groups.iter_mut() { + let first = remap.len() as u32; + let count = group.meshlets.len() as u32; + remap.extend( + group + .meshlets + .iter() + .map(|&m| meshlets.meshlets[m as usize]), + ); + remapped_cull_data.extend(group.meshlets.iter().map(|&m| cull_data[m as usize])); + group.meshlets.resize(2, 0); + group.meshlets[0] = first; + group.meshlets[1] = count; + } + meshlets.meshlets = remap; + *cull_data = remapped_cull_data; + + let mut out = vec![]; + let mut aabb = aabb_default(); + let mut max_depth = 0; + + if self.nodes.len() == 1 { + let mut o = BvhNode::default(); + let group = &groups[0]; + o.aabbs[0] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + o.lod_bounds[0] = sphere_to_meshlet(group.lod_bounds); + o.child_counts[0] = group.meshlets[1] as _; + out.push(o); + aabb = group.aabb; + max_depth = 1; + } else { + let root = self.build_temp(); + let root = self.build_inner(&groups, &mut out, &mut max_depth, root, 1); + assert_eq!(root, 0, "root must be 0"); + + let root = &out[0]; + for i in 0..8 { + if root.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + root.aabbs[i].center, + root.aabbs[i].half_extent, + )); + } + } + + let mut reachable = vec![false; meshlets.meshlets.len()]; + verify_bvh(&out, cull_data, &mut reachable, 0); + assert!( + reachable.iter().all(|&x| x), + "all meshlets must be reachable" + ); + + ( + out, + MeshletAabb { + center: aabb.center().into(), + half_extent: aabb.half_size().into(), + }, + max_depth, + ) + } +} + +fn verify_bvh( + out: &[BvhNode], + cull_data: &[TempMeshletCullData], + reachable: &mut [bool], + node: u32, +) { + let node = &out[node as usize]; + for i in 0..8 { + let sphere = node.lod_bounds[i]; + let error = node.aabbs[i].error; + if node.child_counts[i] == u8::MAX { + let child = &out[node.aabbs[i].child_offset as usize]; + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + assert!( + child.aabbs[i].error <= error, + "BVH errors are not monotonic" + ); + let sphere_error = (sphere.center - child.lod_bounds[i].center).length() + - (sphere.radius - child.lod_bounds[i].radius); + assert!( + sphere_error <= 0.0001, + "BVH lod spheres are not monotonic ({sphere_error})" + ); + } + verify_bvh(out, cull_data, reachable, node.aabbs[i].child_offset); + } else { + for m in 0..node.child_counts[i] as u32 { + let mid = (m + node.aabbs[i].child_offset) as usize; + let meshlet = &cull_data[mid]; + assert!(meshlet.error <= error, "meshlet errors are not monotonic"); + let sphere_error = (Vec3A::from(sphere.center) - meshlet.lod_group_sphere.center) + .length() + - (sphere.radius - meshlet.lod_group_sphere.radius()); + assert!( + sphere_error <= 0.0001, + "meshlet lod spheres are not monotonic: ({sphere_error})" + ); + reachable[mid] = true; + } + } + } +} + +fn aabb_default() -> Aabb3d { + Aabb3d { + min: Vec3A::INFINITY, + max: Vec3A::NEG_INFINITY, + } +} + +fn aabb_to_meshlet(aabb: Aabb3d, error: f32, child_offset: u32) -> MeshletAabbErrorOffset { + MeshletAabbErrorOffset { + center: aabb.center().into(), + error, + half_extent: aabb.half_size().into(), + child_offset, + } +} + +fn sphere_to_meshlet(sphere: BoundingSphere) -> MeshletBoundingSphere { MeshletBoundingSphere { - center: bounds.center.into(), - radius: bounds.radius, + center: sphere.center.into(), + radius: sphere.radius(), } } @@ -726,8 +1104,8 @@ fn pack2x16snorm(v: Vec2) -> u32 { pub enum MeshToMeshletMeshConversionError { #[error("Mesh primitive topology is not TriangleList")] WrongMeshPrimitiveTopology, - #[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}")] - WrongMeshVertexAttributes, + #[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}: {0:?}")] + WrongMeshVertexAttributes(Vec), #[error("Mesh has no indices")] MeshMissingIndices, } diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index 661d4791ae..94d03a925a 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -1,8 +1,9 @@ use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d}; +use crate::DUMMY_MESH_MATERIAL; use crate::{ - material::DUMMY_MESH_MATERIAL, Material, MaterialBindingId, MeshFlags, MeshTransforms, - MeshUniform, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, - RenderMaterialBindings, RenderMaterialInstances, + meshlet::asset::MeshletAabb, MaterialBindingId, MeshFlags, MeshTransforms, MeshUniform, + NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, RenderMaterialBindings, + RenderMaterialInstances, }; use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId}; use bevy_ecs::{ @@ -17,32 +18,33 @@ use bevy_render::{ render_resource::StorageBuffer, sync_world::MainEntity, view::RenderLayers, MainWorld, }; use bevy_transform::components::GlobalTransform; -use core::ops::{DerefMut, Range}; +use core::ops::DerefMut; /// Manages data for each entity with a [`MeshletMesh`]. #[derive(Resource)] pub struct InstanceManager { /// Amount of instances in the scene. pub scene_instance_count: u32, - /// Amount of clusters in the scene. - pub scene_cluster_count: u32, + /// The max BVH depth of any instance in the scene. This is used to control the number of + /// dependent dispatches emitted for BVH traversal. + pub max_bvh_depth: u32, /// Per-instance [`MainEntity`], [`RenderLayers`], and [`NotShadowCaster`]. pub instances: Vec<(MainEntity, RenderLayers, bool)>, /// Per-instance [`MeshUniform`]. pub instance_uniforms: StorageBuffer>, + /// Per-instance model-space AABB. + pub instance_aabbs: StorageBuffer>, /// Per-instance material ID. pub instance_material_ids: StorageBuffer>, - /// Per-instance count of meshlets in the instance's [`MeshletMesh`]. - pub instance_meshlet_counts: StorageBuffer>, - /// Per-instance index to the start of the instance's slice of the meshlets buffer. - pub instance_meshlet_slice_starts: StorageBuffer>, + /// Per-instance index to the root node of the instance's BVH. + pub instance_bvh_root_nodes: StorageBuffer>, /// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support. pub view_instance_visibility: EntityHashMap>>, - /// Next material ID available for a [`Material`]. + /// Next material ID available. next_material_id: u32, - /// Map of [`Material`] to material ID. + /// Map of material asset to material ID. material_id_lookup: HashMap, /// Set of material IDs used in the scene. material_ids_present_in_scene: HashSet, @@ -52,7 +54,7 @@ impl InstanceManager { pub fn new() -> Self { Self { scene_instance_count: 0, - scene_cluster_count: 0, + max_bvh_depth: 0, instances: Vec::new(), instance_uniforms: { @@ -60,19 +62,19 @@ impl InstanceManager { buffer.set_label(Some("meshlet_instance_uniforms")); buffer }, + instance_aabbs: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_aabbs")); + buffer + }, instance_material_ids: { let mut buffer = StorageBuffer::default(); buffer.set_label(Some("meshlet_instance_material_ids")); buffer }, - instance_meshlet_counts: { + instance_bvh_root_nodes: { let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_meshlet_counts")); - buffer - }, - instance_meshlet_slice_starts: { - let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_meshlet_slice_starts")); + buffer.set_label(Some("meshlet_instance_bvh_root_nodes")); buffer }, view_instance_visibility: EntityHashMap::default(), @@ -86,7 +88,9 @@ impl InstanceManager { pub fn add_instance( &mut self, instance: MainEntity, - meshlets_slice: Range, + root_bvh_node: u32, + aabb: MeshletAabb, + bvh_depth: u32, transform: &GlobalTransform, previous_transform: Option<&PreviousGlobalTransform>, render_layers: Option<&RenderLayers>, @@ -139,16 +143,12 @@ impl InstanceManager { not_shadow_caster, )); self.instance_uniforms.get_mut().push(mesh_uniform); + self.instance_aabbs.get_mut().push(aabb); self.instance_material_ids.get_mut().push(0); - self.instance_meshlet_counts - .get_mut() - .push(meshlets_slice.len() as u32); - self.instance_meshlet_slice_starts - .get_mut() - .push(meshlets_slice.start); + self.instance_bvh_root_nodes.get_mut().push(root_bvh_node); self.scene_instance_count += 1; - self.scene_cluster_count += meshlets_slice.len() as u32; + self.max_bvh_depth = self.max_bvh_depth.max(bvh_depth); } /// Get the material ID for a [`crate::Material`]. @@ -168,13 +168,13 @@ impl InstanceManager { pub fn reset(&mut self, entities: &Entities) { self.scene_instance_count = 0; - self.scene_cluster_count = 0; + self.max_bvh_depth = 0; self.instances.clear(); self.instance_uniforms.get_mut().clear(); + self.instance_aabbs.get_mut().clear(); self.instance_material_ids.get_mut().clear(); - self.instance_meshlet_counts.get_mut().clear(); - self.instance_meshlet_slice_starts.get_mut().clear(); + self.instance_bvh_root_nodes.get_mut().clear(); self.view_instance_visibility .retain(|view_entity, _| entities.contains(*view_entity)); self.view_instance_visibility @@ -233,6 +233,7 @@ pub fn extract_meshlet_mesh_entities( } // Iterate over every instance + // TODO: Switch to change events to not upload every instance every frame. for ( instance, meshlet_mesh, @@ -252,13 +253,15 @@ pub fn extract_meshlet_mesh_entities( } // Upload the instance's MeshletMesh asset data if not done already done - let meshlets_slice = + let (root_bvh_node, aabb, bvh_depth) = meshlet_mesh_manager.queue_upload_if_needed(meshlet_mesh.id(), &mut assets); // Add the instance's data to the instance manager instance_manager.add_instance( instance.into(), - meshlets_slice, + root_bvh_node, + aabb, + bvh_depth, transform, previous_transform, render_layers, @@ -272,7 +275,7 @@ pub fn extract_meshlet_mesh_entities( /// For each entity in the scene, record what material ID its material was assigned in the `prepare_material_meshlet_meshes` systems, /// and note that the material is used by at least one entity in the scene. -pub fn queue_material_meshlet_meshes( +pub fn queue_material_meshlet_meshes( mut instance_manager: ResMut, render_material_instances: Res, ) { @@ -280,16 +283,14 @@ pub fn queue_material_meshlet_meshes( for (i, (instance, _, _)) in instance_manager.instances.iter().enumerate() { if let Some(material_instance) = render_material_instances.instances.get(instance) { - if let Ok(material_asset_id) = material_instance.asset_id.try_typed::() { - if let Some(material_id) = instance_manager - .material_id_lookup - .get(&material_asset_id.untyped()) - { - instance_manager - .material_ids_present_in_scene - .insert(*material_id); - instance_manager.instance_material_ids.get_mut()[i] = *material_id; - } + if let Some(material_id) = instance_manager + .material_id_lookup + .get(&material_instance.asset_id) + { + instance_manager + .material_ids_present_in_scene + .insert(*material_id); + instance_manager.instance_material_ids.get_mut()[i] = *material_id; } } } diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 57762bfc8a..1ec316dcd8 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -1,27 +1,25 @@ use super::{ - instance_manager::InstanceManager, resource_manager::ResourceManager, - MESHLET_MESH_MATERIAL_SHADER_HANDLE, + instance_manager::InstanceManager, pipelines::MeshletPipelines, + resource_manager::ResourceManager, }; -use crate::{ - environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, - material_bind_groups::MaterialBindGroupAllocator, *, -}; -use bevy_asset::AssetServer; +use crate::{irradiance_volume::IrradianceVolume, *}; use bevy_core_pipeline::{ core_3d::Camera3d, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; +use bevy_light::EnvironmentMapLight; use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::{ camera::TemporalJitter, mesh::{Mesh, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts}, - render_asset::RenderAssets, render_resource::*, view::ExtractedView, }; -use core::hash::Hash; +use bevy_utils::default; +use core::any::{Any, TypeId}; /// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletMainOpaquePass3dNode`]. #[derive(Component, Deref, DerefMut, Default)] @@ -29,17 +27,17 @@ pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipeline /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletMainOpaquePass3dNode`], /// and register the material with [`InstanceManager`]. -pub fn prepare_material_meshlet_meshes_main_opaque_pass( +pub fn prepare_material_meshlet_meshes_main_opaque_pass( resource_manager: ResMut, mut instance_manager: ResMut, - mut cache: Local>, + mut cache: Local>, pipeline_cache: Res, - material_pipeline: Res>, + material_pipeline: Res, mesh_pipeline: Res, - render_materials: Res>>, + render_materials: Res>, + meshlet_pipelines: Res, render_material_instances: Res, - material_bind_group_allocator: Res>, - asset_server: Res, + material_bind_group_allocators: Res, mut mesh_vertex_buffer_layouts: ResMut, mut views: Query< ( @@ -62,9 +60,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( ), With, >, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); for ( @@ -151,17 +147,12 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( for material_id in render_material_instances .instances .values() - .flat_map(|instance| instance.asset_id.try_typed::().ok()) + .map(|instance| instance.asset_id) .collect::>() { let Some(material) = render_materials.get(material_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - continue; - }; if material.properties.render_method != OpaqueRendererMethod::Forward || material.properties.alpha_mode != AlphaMode::Opaque @@ -170,15 +161,18 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( continue; } - let Ok(material_pipeline_descriptor) = material_pipeline.specialize( - MaterialPipelineKey { - mesh_key: view_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, - fake_vertex_buffer_layout, - ) else { + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: material_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { continue; }; let material_fragment = material_pipeline_descriptor.fragment.unwrap(); @@ -186,16 +180,25 @@ 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 + .properties + .material_layout + .as_ref() + .unwrap() + .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, + shader: meshlet_pipelines.meshlet_mesh_material.clone(), shader_defs: shader_defs.clone(), entry_point: material_pipeline_descriptor.vertex.entry_point, buffers: Vec::new(), @@ -210,10 +213,9 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( }), multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: match M::meshlet_mesh_fragment_shader() { - ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, - ShaderRef::Handle(handle) => handle, - ShaderRef::Path(path) => asset_server.load(path), + shader: match material.properties.get_shader(MeshletFragmentShader) { + Some(shader) => shader.clone(), + None => meshlet_pipelines.meshlet_mesh_material.clone(), }, shader_defs, entry_point: material_fragment.entry_point, @@ -221,10 +223,14 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( }), zero_initialize_workgroup_memory: false, }; + let type_id = material_id.type_id(); + let Some(material_bind_group_allocator) = material_bind_group_allocators.get(&type_id) + else { + continue; + }; + let material_id = instance_manager.get_material_id(material_id); - let material_id = instance_manager.get_material_id(material_id.untyped()); - - let pipeline_id = *cache.entry(view_key).or_insert_with(|| { + let pipeline_id = *cache.entry((view_key, type_id)).or_insert_with(|| { pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) }); @@ -254,17 +260,17 @@ pub struct MeshletViewMaterialsDeferredGBufferPrepass( /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`], /// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`InstanceManager`]. -pub fn prepare_material_meshlet_meshes_prepass( +pub fn prepare_material_meshlet_meshes_prepass( resource_manager: ResMut, mut instance_manager: ResMut, - mut cache: Local>, + mut cache: Local>, pipeline_cache: Res, - prepass_pipeline: Res>, - render_materials: Res>>, + prepass_pipeline: Res, + material_bind_group_allocators: Res, + render_materials: Res>, + meshlet_pipelines: Res, render_material_instances: Res, mut mesh_vertex_buffer_layouts: ResMut, - material_bind_group_allocator: Res>, - asset_server: Res, mut views: Query< ( &mut MeshletViewMaterialsPrepass, @@ -274,9 +280,7 @@ pub fn prepare_material_meshlet_meshes_prepass( ), With, >, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); for ( @@ -301,14 +305,14 @@ pub fn prepare_material_meshlet_meshes_prepass( for material_id in render_material_instances .instances .values() - .flat_map(|instance| instance.asset_id.try_typed::().ok()) + .map(|instance| instance.asset_id) .collect::>() { let Some(material) = render_materials.get(material_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_id.type_id()) else { continue; }; @@ -329,15 +333,18 @@ pub fn prepare_material_meshlet_meshes_prepass( continue; } - let Ok(material_pipeline_descriptor) = prepass_pipeline.specialize( - MaterialPipelineKey { - mesh_key: view_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, - fake_vertex_buffer_layout, - ) else { + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { continue; }; let material_fragment = material_pipeline_descriptor.fragment.unwrap(); @@ -346,38 +353,47 @@ pub fn prepare_material_meshlet_meshes_prepass( shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); let view_layout = if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { - prepass_pipeline.internal.view_layout_motion_vectors.clone() + prepass_pipeline.view_layout_motion_vectors.clone() } else { - prepass_pipeline - .internal - .view_layout_no_motion_vectors - .clone() + prepass_pipeline.view_layout_no_motion_vectors.clone() }; let fragment_shader = if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - M::meshlet_mesh_deferred_fragment_shader() + material + .properties + .get_shader(MeshletDeferredFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) } else { - M::meshlet_mesh_prepass_fragment_shader() + material + .properties + .get_shader(MeshletPrepassFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) }; - let entry_point = match fragment_shader { - ShaderRef::Default => "prepass_fragment".into(), - _ => material_fragment.entry_point, + let entry_point = if fragment_shader == meshlet_pipelines.meshlet_mesh_material { + material_fragment.entry_point.clone() + } else { + None }; let pipeline_descriptor = RenderPipelineDescriptor { label: material_pipeline_descriptor.label, layout: vec![ view_layout, + prepass_pipeline.empty_layout.clone(), resource_manager.material_shade_bind_group_layout.clone(), - prepass_pipeline.internal.material_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), ], - push_constant_ranges: vec![], vertex: VertexState { - shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE, + shader: meshlet_pipelines.meshlet_mesh_material.clone(), shader_defs: shader_defs.clone(), entry_point: material_pipeline_descriptor.vertex.entry_point, - buffers: Vec::new(), + ..default() }, primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { @@ -387,25 +403,22 @@ pub fn prepare_material_meshlet_meshes_prepass( stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: match fragment_shader { - ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, - ShaderRef::Handle(handle) => handle, - ShaderRef::Path(path) => asset_server.load(path), - }, + shader: fragment_shader, shader_defs, entry_point, targets: material_fragment.targets, }), - zero_initialize_workgroup_memory: false, + ..default() }; - let material_id = instance_manager.get_material_id(material_id.untyped()); + let material_id = instance_manager.get_material_id(material_id); - let pipeline_id = *cache.entry(view_key).or_insert_with(|| { - pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) - }); + let pipeline_id = *cache + .entry((view_key, material_id.type_id())) + .or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) diff --git a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs index 9c2d432d88..39dcb0c169 100644 --- a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs +++ b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs @@ -18,7 +18,7 @@ use bevy_ecs::{ world::World, }; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_resource::{ LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, @@ -42,6 +42,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { &'static ViewLightProbesUniformOffset, &'static ViewScreenSpaceReflectionsUniformOffset, &'static ViewEnvironmentMapUniformOffset, + Option<&'static MainPassResolutionOverride>, &'static MeshletViewMaterialsMainOpaquePass, &'static MeshletViewBindGroups, &'static MeshletViewResources, @@ -61,6 +62,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { view_light_probes_offset, view_ssr_offset, view_environment_map_offset, + resolution_override, meshlet_view_materials, meshlet_view_bind_groups, meshlet_view_resources, @@ -101,12 +103,12 @@ impl ViewNode for MeshletMainOpaquePass3dNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } render_pass.set_bind_group( 0, - &mesh_view_bind_group.value, + &mesh_view_bind_group.main, &[ view_uniform_offset.offset, view_lights_offset.offset, @@ -116,7 +118,8 @@ impl ViewNode for MeshletMainOpaquePass3dNode { **view_environment_map_offset, ], ); - render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); + render_pass.set_bind_group(1, &mesh_view_bind_group.binding_array, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in @@ -128,7 +131,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { { let x = *material_id * 3; render_pass.set_render_pipeline(material_pipeline); - render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.set_bind_group(3, material_bind_group, &[]); render_pass.draw(x..(x + 3), 0..1); } } @@ -147,6 +150,7 @@ impl ViewNode for MeshletPrepassNode { &'static ViewPrepassTextures, &'static ViewUniformOffset, &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, Has, &'static MeshletViewMaterialsPrepass, &'static MeshletViewBindGroups, @@ -162,6 +166,7 @@ impl ViewNode for MeshletPrepassNode { view_prepass_textures, view_uniform_offset, previous_view_uniform_offset, + resolution_override, view_has_motion_vector_prepass, meshlet_view_materials, meshlet_view_bind_groups, @@ -219,7 +224,7 @@ impl ViewNode for MeshletPrepassNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if view_has_motion_vector_prepass { @@ -239,7 +244,8 @@ impl ViewNode for MeshletPrepassNode { ); } - render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in @@ -270,6 +276,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { &'static ViewPrepassTextures, &'static ViewUniformOffset, &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, Has, &'static MeshletViewMaterialsDeferredGBufferPrepass, &'static MeshletViewBindGroups, @@ -285,6 +292,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { view_prepass_textures, view_uniform_offset, previous_view_uniform_offset, + resolution_override, view_has_motion_vector_prepass, meshlet_view_materials, meshlet_view_bind_groups, @@ -347,7 +355,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if view_has_motion_vector_prepass { @@ -367,7 +375,8 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { ); } - render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in diff --git a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl index e179e78b7a..4533b2bd7f 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl +++ b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl @@ -5,6 +5,13 @@ #import bevy_pbr::prepass_bindings::PreviousViewUniforms #import bevy_pbr::utils::octahedral_decode_signed +struct BvhNode { + aabbs: array, + lod_bounds: array, 8>, + child_counts: array, + _padding: vec2, +} + struct Meshlet { start_vertex_position_bit: u32, start_vertex_attribute_id: u32, @@ -24,15 +31,34 @@ fn get_meshlet_triangle_count(meshlet: ptr) -> u32 { return extractBits((*meshlet).packed_a, 8u, 8u); } -struct MeshletBoundingSpheres { - culling_sphere: MeshletBoundingSphere, - lod_group_sphere: MeshletBoundingSphere, - lod_parent_group_sphere: MeshletBoundingSphere, +struct MeshletCullData { + aabb: MeshletAabbErrorOffset, + lod_group_sphere: vec4, } -struct MeshletBoundingSphere { +struct MeshletAabb { center: vec3, - radius: f32, + half_extent: vec3, +} + +struct MeshletAabbErrorOffset { + center_and_error: vec4, + half_extent_and_child_offset: vec4, +} + +fn get_aabb(aabb: ptr) -> MeshletAabb { + return MeshletAabb( + (*aabb).center_and_error.xyz, + (*aabb).half_extent_and_child_offset.xyz, + ); +} + +fn get_aabb_error(aabb: ptr) -> f32 { + return (*aabb).center_and_error.w; +} + +fn get_aabb_child_offset(aabb: ptr) -> u32 { + return bitcast((*aabb).half_extent_and_child_offset.w); } struct DispatchIndirectArgs { @@ -48,63 +74,133 @@ struct DrawIndirectArgs { first_instance: u32, } +// Either a BVH node or a meshlet, along with the instance it is associated with. +// Refers to BVH nodes in `meshlet_bvh_cull_queue` and `meshlet_second_pass_bvh_queue`, where `offset` is the index into `meshlet_bvh_nodes`. +// Refers to meshlets in `meshlet_meshlet_cull_queue` and `meshlet_raster_clusters`. +// In `meshlet_meshlet_cull_queue`, `offset` is the index into `meshlet_cull_data`. +// In `meshlet_raster_clusters`, `offset` is the index into `meshlets`. +struct InstancedOffset { + instance_id: u32, + offset: u32, +} + const CENTIMETERS_PER_METER = 100.0; -#ifdef MESHLET_FILL_CLUSTER_BUFFERS_PASS -var scene_instance_count: u32; -@group(0) @binding(0) var meshlet_instance_meshlet_counts: array; // Per entity instance -@group(0) @binding(1) var meshlet_instance_meshlet_slice_starts: array; // Per entity instance -@group(0) @binding(2) var meshlet_cluster_instance_ids: array; // Per cluster -@group(0) @binding(3) var meshlet_cluster_meshlet_ids: array; // Per cluster -@group(0) @binding(4) var meshlet_global_cluster_count: atomic; // Single object shared between all workgroups +#ifdef MESHLET_INSTANCE_CULLING_PASS +struct Constants { scene_instance_count: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Per entity instance data +@group(0) @binding(3) var meshlet_instance_uniforms: array; +@group(0) @binding(4) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask +@group(0) @binding(5) var meshlet_instance_aabbs: array; +@group(0) @binding(6) var meshlet_instance_bvh_root_nodes: array; + +// BVH cull queue data +@group(0) @binding(7) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(8) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(9) var meshlet_bvh_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_second_pass_instance_count: atomic; +@group(0) @binding(11) var meshlet_second_pass_instance_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_second_pass_instance_candidates: array; +#else +@group(0) @binding(10) var meshlet_second_pass_instance_count: u32; +@group(0) @binding(11) var meshlet_second_pass_instance_candidates: array; +#endif #endif -#ifdef MESHLET_CULLING_PASS -struct Constants { scene_cluster_count: u32, meshlet_raster_cluster_rightmost_slot: u32 } +#ifdef MESHLET_BVH_CULLING_PASS +struct Constants { read_from_front: u32, rightmost_slot: u32 } var constants: Constants; -@group(0) @binding(0) var meshlet_cluster_meshlet_ids: array; // Per cluster -@group(0) @binding(1) var meshlet_bounding_spheres: array; // Per meshlet -@group(0) @binding(2) var meshlet_simplification_errors: array; // Per meshlet -@group(0) @binding(3) var meshlet_cluster_instance_ids: array; // Per cluster -@group(0) @binding(4) var meshlet_instance_uniforms: array; // Per entity instance -@group(0) @binding(5) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask -@group(0) @binding(6) var meshlet_second_pass_candidates: array>; // 1 bit per cluster , packed as a bitmask -@group(0) @binding(7) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; // Single object shared between all workgroups -@group(0) @binding(8) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; // Single object shared between all workgroups -@group(0) @binding(9) var meshlet_raster_clusters: array; // Single object shared between all workgroups -@group(0) @binding(10) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass -@group(0) @binding(11) var view: View; -@group(0) @binding(12) var previous_view: PreviousViewUniforms; -fn should_cull_instance(instance_id: u32) -> bool { - let bit_offset = instance_id % 32u; - let packed_visibility = meshlet_view_instance_visibility[instance_id / 32u]; - return bool(extractBits(packed_visibility, bit_offset, 1u)); -} +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; -// TODO: Load 4x per workgroup instead of once per thread? -fn cluster_is_second_pass_candidate(cluster_id: u32) -> bool { - let packed_candidates = meshlet_second_pass_candidates[cluster_id / 32u]; - let bit_offset = cluster_id % 32u; - return bool(extractBits(packed_candidates, bit_offset, 1u)); -} +// Global mesh data +@group(0) @binding(3) var meshlet_bvh_nodes: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// BVH cull queue data +@group(0) @binding(5) var meshlet_bvh_cull_count_read: u32; +@group(0) @binding(6) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(7) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(8) var meshlet_bvh_cull_queue: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_early: atomic; +@group(0) @binding(10) var meshlet_meshlet_cull_count_late: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch_early: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_dispatch_late: DispatchIndirectArgs; +@group(0) @binding(13) var meshlet_meshlet_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(14) var meshlet_second_pass_bvh_count: atomic; +@group(0) @binding(15) var meshlet_second_pass_bvh_dispatch: DispatchIndirectArgs; +@group(0) @binding(16) var meshlet_second_pass_bvh_queue: array; +#endif +#endif + +#ifdef MESHLET_CLUSTER_CULLING_PASS +struct Constants { rightmost_slot: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Global mesh data +@group(0) @binding(3) var meshlet_cull_data: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// Raster queue data +@group(0) @binding(5) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(6) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(7) var meshlet_previous_raster_counts: array; +@group(0) @binding(8) var meshlet_raster_clusters: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_read: u32; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_meshlet_cull_count_write: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_queue: array; +#else +@group(0) @binding(10) var meshlet_meshlet_cull_queue: array; +#endif #endif #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS -@group(0) @binding(0) var meshlet_cluster_meshlet_ids: array; // Per cluster +@group(0) @binding(0) var meshlet_raster_clusters: array; // Per cluster @group(0) @binding(1) var meshlets: array; // Per meshlet @group(0) @binding(2) var meshlet_indices: array; // Many per meshlet @group(0) @binding(3) var meshlet_vertex_positions: array; // Many per meshlet -@group(0) @binding(4) var meshlet_cluster_instance_ids: array; // Per cluster -@group(0) @binding(5) var meshlet_instance_uniforms: array; // Per entity instance -@group(0) @binding(6) var meshlet_raster_clusters: array; // Single object shared between all workgroups -@group(0) @binding(7) var meshlet_software_raster_cluster_count: u32; +@group(0) @binding(4) var meshlet_instance_uniforms: array; // Per entity instance +@group(0) @binding(5) var meshlet_previous_raster_counts: array; +@group(0) @binding(6) var meshlet_software_raster_cluster_count: u32; #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT -@group(0) @binding(8) var meshlet_visibility_buffer: texture_storage_2d; +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; #else -@group(0) @binding(8) var meshlet_visibility_buffer: texture_storage_2d; +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; #endif -@group(0) @binding(9) var view: View; +@group(0) @binding(8) var view: View; // 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 { @@ -149,15 +245,14 @@ 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_raster_clusters: 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_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/meshlet_cull_shared.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl new file mode 100644 index 0000000000..975dd74f1c --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl @@ -0,0 +1,207 @@ +#define_import_path bevy_pbr::meshlet_cull_shared + +#import bevy_pbr::meshlet_bindings::{ + MeshletAabb, + DispatchIndirectArgs, + InstancedOffset, + depth_pyramid, + view, + previous_view, + meshlet_instance_uniforms, +} +#import bevy_render::maths::affine3_to_square + +// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115 +fn lod_error_is_imperceptible(lod_sphere: vec4, simplification_error: f32, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2]))); + let camera_pos = view.world_position; + + let projection = view.clip_from_view; + if projection[3][3] == 1.0 { + // Orthographic + let world_error = simplification_error * world_scale; + let proj = projection[1][1]; + let height = 2.0 / proj; + let norm_error = world_error / height; + return norm_error * view.viewport.w < 1.0; + } else { + // Perspective + var near = projection[3][2]; + let world_sphere_center = (world_from_local * vec4(lod_sphere.xyz, 1.0)).xyz; + let world_sphere_radius = lod_sphere.w * world_scale; + let d_pos = world_sphere_center - camera_pos; + let d = sqrt(dot(d_pos, d_pos)) - world_sphere_radius; + let norm_error = simplification_error / max(d, near) * projection[1][1] * 0.5; + return norm_error * view.viewport.w < 1.0; + } +} + +fn normalize_plane(p: vec4) -> vec4 { + return p / length(p.xyz); +} + +// https://fgiesen.wordpress.com/2012/08/31/frustum-planes-from-the-projection-matrix/ +// https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling/ +fn aabb_in_frustum(aabb: MeshletAabb, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let row_major = transpose(clip_from_local); + let planes = array( + row_major[3] + row_major[0], + row_major[3] - row_major[0], + row_major[3] + row_major[1], + row_major[3] - row_major[1], + row_major[2], + ); + + for (var i = 0; i < 5; i++) { + let plane = normalize_plane(planes[i]); + let flipped = aabb.half_extent * sign(plane.xyz); + if dot(aabb.center + flipped, plane.xyz) <= -plane.w { + return false; + } + } + return true; +} + +struct ScreenAabb { + min: vec3, + max: vec3, +} + +fn min8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +fn max8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return max(max(max(a, b), max(c, d)), max(max(e, f), max(g, h))); +} + +fn min8_4(a: vec4, b: vec4, c: vec4, d: vec4, e: vec4, f: vec4, g: vec4, h: vec4) -> vec4 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +// https://zeux.io/2023/01/12/approximate-projected-bounds/ +fn project_aabb(clip_from_local: mat4x4, near: f32, aabb: MeshletAabb, out: ptr) -> bool { + let extent = aabb.half_extent * 2.0; + let sx = clip_from_local * vec4(extent.x, 0.0, 0.0, 0.0); + let sy = clip_from_local * vec4(0.0, extent.y, 0.0, 0.0); + let sz = clip_from_local * vec4(0.0, 0.0, extent.z, 0.0); + + let p0 = clip_from_local * vec4(aabb.center - aabb.half_extent, 1.0); + let p1 = p0 + sz; + let p2 = p0 + sy; + let p3 = p2 + sz; + let p4 = p0 + sx; + let p5 = p4 + sz; + let p6 = p4 + sy; + let p7 = p6 + sz; + + let depth = min8_4(p0, p1, p2, p3, p4, p5, p6, p7).w; + // do not occlusion cull if we are inside the aabb + if depth < near { + return false; + } + + let dp0 = p0.xyz / p0.w; + let dp1 = p1.xyz / p1.w; + let dp2 = p2.xyz / p2.w; + let dp3 = p3.xyz / p3.w; + let dp4 = p4.xyz / p4.w; + let dp5 = p5.xyz / p5.w; + let dp6 = p6.xyz / p6.w; + let dp7 = p7.xyz / p7.w; + let min = min8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + let max = max8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + var vaabb = vec4(min.xy, max.xy); + // convert ndc to texture coordinates by rescaling and flipping Y + vaabb = vaabb.xwzy * vec4(0.5, -0.5, 0.5, -0.5) + 0.5; + (*out).min = vec3(vaabb.xy, min.z); + (*out).max = vec3(vaabb.zw, max.z); + return true; +} + +fn sample_hzb(smin: vec2, smax: vec2, mip: i32) -> f32 { + let texel = vec4(0, 1, 2, 3); + let sx = min(smin.x + texel, smax.xxxx); + let sy = min(smin.y + texel, smax.yyyy); + // TODO: switch to min samplers when wgpu has them + // sampling 16 times a finer mip is worth the extra cost for better culling + let a = sample_hzb_row(sx, sy.x, mip); + let b = sample_hzb_row(sx, sy.y, mip); + let c = sample_hzb_row(sx, sy.z, mip); + let d = sample_hzb_row(sx, sy.w, mip); + return min(min(a, b), min(c, d)); +} + +fn sample_hzb_row(sx: vec4, sy: u32, mip: i32) -> f32 { + let a = textureLoad(depth_pyramid, vec2(sx.x, sy), mip).x; + let b = textureLoad(depth_pyramid, vec2(sx.y, sy), mip).x; + let c = textureLoad(depth_pyramid, vec2(sx.z, sy), mip).x; + let d = textureLoad(depth_pyramid, vec2(sx.w, sy), mip).x; + return min(min(a, b), min(c, d)); +} + +// TODO: We should probably be using a POT HZB texture? +fn occlusion_cull_screen_aabb(aabb: ScreenAabb, screen: vec2) -> bool { + let hzb_size = ceil(screen * 0.5); + let aabb_min = aabb.min.xy * hzb_size; + let aabb_max = aabb.max.xy * hzb_size; + + let min_texel = vec2(max(aabb_min, vec2(0.0))); + let max_texel = vec2(min(aabb_max, hzb_size - 1.0)); + let size = max_texel - min_texel; + let max_size = max(size.x, size.y); + + // note: add 1 before max because the unsigned overflow behavior is intentional + // it wraps around firstLeadingBit(0) = ~0 to 0 + // TODO: we actually sample a 4x4 block, so ideally this would be `max(..., 3u) - 3u`. + // However, since our HZB is not a power of two, we need to be extra-conservative to not over-cull, so we go up a mip. + var mip = max(firstLeadingBit(max_size) + 1u, 2u) - 2u; + + if any((max_texel >> vec2(mip)) > (min_texel >> vec2(mip)) + 3) { + mip += 1u; + } + + let smin = min_texel >> vec2(mip); + let smax = max_texel >> vec2(mip); + + let curr_depth = sample_hzb(smin, smax, i32(mip)); + return aabb.max.z <= curr_depth; +} + +fn occlusion_cull_projection() -> mat4x4 { +#ifdef FIRST_CULLING_PASS + return view.clip_from_world; +#else + return previous_view.clip_from_world; +#endif +} + +fn occlusion_cull_clip_from_local(instance_id: u32) -> mat4x4 { +#ifdef FIRST_CULLING_PASS + let prev_world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].previous_world_from_local); + return previous_view.clip_from_world * prev_world_from_local; +#else + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + return view.clip_from_world * world_from_local; +#endif +} + +fn should_occlusion_cull_aabb(aabb: MeshletAabb, instance_id: u32) -> bool { + let projection = occlusion_cull_projection(); + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + + let clip_from_local = occlusion_cull_clip_from_local(instance_id); + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + if project_aabb(clip_from_local, near, aabb, &screen_aabb) { + return occlusion_cull_screen_aabb(screen_aabb, view.viewport.zw); + } + return false; +} diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs index 0f4aab7509..93eb5a1afe 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs +++ b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs @@ -1,8 +1,6 @@ -use super::{ - asset::{Meshlet, MeshletBoundingSpheres, MeshletSimplificationError}, - persistent_buffer::PersistentGpuBuffer, - MeshletMesh, -}; +use crate::meshlet::asset::{BvhNode, MeshletAabb, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBuffer, MeshletMesh}; use alloc::sync::Arc; use bevy_asset::{AssetId, Assets}; use bevy_ecs::{ @@ -25,10 +23,11 @@ pub struct MeshletMeshManager { pub vertex_normals: PersistentGpuBuffer>, pub vertex_uvs: PersistentGpuBuffer>, pub indices: PersistentGpuBuffer>, + pub bvh_nodes: PersistentGpuBuffer>, pub meshlets: PersistentGpuBuffer>, - pub meshlet_bounding_spheres: PersistentGpuBuffer>, - pub meshlet_simplification_errors: PersistentGpuBuffer>, - meshlet_mesh_slices: HashMap, [Range; 7]>, + pub meshlet_cull_data: PersistentGpuBuffer>, + meshlet_mesh_slices: + HashMap, ([Range; 7], MeshletAabb, u32)>, } impl FromWorld for MeshletMeshManager { @@ -39,26 +38,21 @@ impl FromWorld for MeshletMeshManager { vertex_normals: PersistentGpuBuffer::new("meshlet_vertex_normals", render_device), vertex_uvs: PersistentGpuBuffer::new("meshlet_vertex_uvs", render_device), indices: PersistentGpuBuffer::new("meshlet_indices", render_device), + bvh_nodes: PersistentGpuBuffer::new("meshlet_bvh_nodes", render_device), meshlets: PersistentGpuBuffer::new("meshlets", render_device), - meshlet_bounding_spheres: PersistentGpuBuffer::new( - "meshlet_bounding_spheres", - render_device, - ), - meshlet_simplification_errors: PersistentGpuBuffer::new( - "meshlet_simplification_errors", - render_device, - ), + meshlet_cull_data: PersistentGpuBuffer::new("meshlet_cull_data", render_device), meshlet_mesh_slices: HashMap::default(), } } } impl MeshletMeshManager { + // Returns the index of the root BVH node, as well as the depth of the BVH. pub fn queue_upload_if_needed( &mut self, asset_id: AssetId, assets: &mut Assets, - ) -> Range { + ) -> (u32, MeshletAabb, u32) { let queue_meshlet_mesh = |asset_id: &AssetId| { let meshlet_mesh = assets.remove_untracked(*asset_id).expect( "MeshletMesh asset was already unloaded but is not registered with MeshletMeshManager", @@ -84,51 +78,59 @@ impl MeshletMeshManager { indices_slice.start, ), ); - let meshlet_bounding_spheres_slice = self - .meshlet_bounding_spheres - .queue_write(Arc::clone(&meshlet_mesh.meshlet_bounding_spheres), ()); - let meshlet_simplification_errors_slice = self - .meshlet_simplification_errors - .queue_write(Arc::clone(&meshlet_mesh.meshlet_simplification_errors), ()); + let base_meshlet_index = (meshlets_slice.start / size_of::() as u64) as u32; + let bvh_node_slice = self + .bvh_nodes + .queue_write(Arc::clone(&meshlet_mesh.bvh), base_meshlet_index); + let meshlet_cull_data_slice = self + .meshlet_cull_data + .queue_write(Arc::clone(&meshlet_mesh.meshlet_cull_data), ()); - [ - vertex_positions_slice, - vertex_normals_slice, - vertex_uvs_slice, - indices_slice, - meshlets_slice, - meshlet_bounding_spheres_slice, - meshlet_simplification_errors_slice, - ] + ( + [ + vertex_positions_slice, + vertex_normals_slice, + vertex_uvs_slice, + indices_slice, + bvh_node_slice, + meshlets_slice, + meshlet_cull_data_slice, + ], + meshlet_mesh.aabb, + meshlet_mesh.bvh_depth, + ) }; // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading - let [_, _, _, _, meshlets_slice, _, _] = self + let ([_, _, _, _, bvh_node_slice, _, _], aabb, bvh_depth) = self .meshlet_mesh_slices .entry(asset_id) .or_insert_with_key(queue_meshlet_mesh) .clone(); - let meshlets_slice_start = meshlets_slice.start as u32 / size_of::() as u32; - let meshlets_slice_end = meshlets_slice.end as u32 / size_of::() as u32; - meshlets_slice_start..meshlets_slice_end + ( + (bvh_node_slice.start / size_of::() as u64) as u32, + aabb, + bvh_depth, + ) } pub fn remove(&mut self, asset_id: &AssetId) { - if let Some( - [vertex_positions_slice, vertex_normals_slice, vertex_uvs_slice, indices_slice, meshlets_slice, meshlet_bounding_spheres_slice, meshlet_simplification_errors_slice], - ) = self.meshlet_mesh_slices.remove(asset_id) + if let Some(( + [vertex_positions_slice, vertex_normals_slice, vertex_uvs_slice, indices_slice, bvh_node_slice, meshlets_slice, meshlet_cull_data_slice], + _, + _, + )) = self.meshlet_mesh_slices.remove(asset_id) { self.vertex_positions .mark_slice_unused(vertex_positions_slice); self.vertex_normals.mark_slice_unused(vertex_normals_slice); self.vertex_uvs.mark_slice_unused(vertex_uvs_slice); self.indices.mark_slice_unused(indices_slice); + self.bvh_nodes.mark_slice_unused(bvh_node_slice); self.meshlets.mark_slice_unused(meshlets_slice); - self.meshlet_bounding_spheres - .mark_slice_unused(meshlet_bounding_spheres_slice); - self.meshlet_simplification_errors - .mark_slice_unused(meshlet_simplification_errors_slice); + self.meshlet_cull_data + .mark_slice_unused(meshlet_cull_data_slice); } } } @@ -151,13 +153,13 @@ pub fn perform_pending_meshlet_mesh_writes( meshlet_mesh_manager .indices .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .bvh_nodes + .perform_writes(&render_queue, &render_device); meshlet_mesh_manager .meshlets .perform_writes(&render_queue, &render_device); meshlet_mesh_manager - .meshlet_bounding_spheres - .perform_writes(&render_queue, &render_device); - meshlet_mesh_manager - .meshlet_simplification_errors + .meshlet_cull_data .perform_writes(&render_queue, &render_device); } diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 2375894613..94b623a280 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -58,7 +58,7 @@ use self::{ }; use crate::{graph::NodePbr, PreviousGlobalTransform}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetId, Handle}; +use bevy_asset::{embedded_asset, AssetApp, AssetId, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, prepass::{DeferredPrepass, MotionVectorPrepass, NormalPrepass}, @@ -74,8 +74,8 @@ use bevy_ecs::{ }; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::Shader, + load_shader_library, + render_graph::{RenderGraphExt, ViewNodeRunner}, renderer::RenderDevice, settings::WgpuFeatures, view::{self, prepare_view_targets, Msaa, Visibility, VisibilityClass}, @@ -85,11 +85,6 @@ use bevy_transform::components::Transform; use derive_more::From; use tracing::error; -const MESHLET_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("d90ac78c-500f-48aa-b488-cc98eb3f6314"); -const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = - weak_handle!("db8d9001-6ca7-4d00-968a-d5f5b96b89c3"); - /// Provides a plugin for rendering large amounts of high-poly 3d meshes using an efficient GPU-driven method. See also [`MeshletMesh`]. /// /// Rendering dense scenes made of high-poly meshes with thousands or millions of triangles is extremely expensive in Bevy's standard renderer. @@ -152,66 +147,19 @@ impl Plugin for MeshletPlugin { std::process::exit(1); } - load_internal_asset!( - app, - MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE, - "clear_visibility_buffer.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_BINDINGS_SHADER_HANDLE, - "meshlet_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - super::MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE, - "visibility_buffer_resolve.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, - "fill_cluster_buffers.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_CULLING_SHADER_HANDLE, - "cull_clusters.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, - "visibility_buffer_software_raster.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, - "visibility_buffer_hardware_raster.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_MESH_MATERIAL_SHADER_HANDLE, - "meshlet_mesh_material.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, - "resolve_render_targets.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE, - "remap_1d_to_2d_dispatch.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "meshlet_bindings.wgsl"); + load_shader_library!(app, "visibility_buffer_resolve.wgsl"); + load_shader_library!(app, "meshlet_cull_shared.wgsl"); + embedded_asset!(app, "clear_visibility_buffer.wgsl"); + embedded_asset!(app, "cull_instances.wgsl"); + embedded_asset!(app, "cull_bvh.wgsl"); + embedded_asset!(app, "cull_clusters.wgsl"); + embedded_asset!(app, "visibility_buffer_software_raster.wgsl"); + embedded_asset!(app, "visibility_buffer_hardware_raster.wgsl"); + embedded_asset!(app, "meshlet_mesh_material.wgsl"); + embedded_asset!(app, "resolve_render_targets.wgsl"); + embedded_asset!(app, "remap_1d_to_2d_dispatch.wgsl"); + embedded_asset!(app, "fill_counts.wgsl"); app.init_asset::() .register_asset_loader(MeshletMeshLoader); @@ -283,6 +231,10 @@ impl Plugin for MeshletPlugin { .in_set(RenderSystems::ManageViews), prepare_meshlet_per_frame_resources.in_set(RenderSystems::PrepareResources), prepare_meshlet_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), + queue_material_meshlet_meshes.in_set(RenderSystems::QueueMeshes), + prepare_material_meshlet_meshes_main_opaque_pass + .in_set(RenderSystems::QueueMeshes) + .before(queue_material_meshlet_meshes), ), ); } diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs index 85dec457f9..e8f4669227 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs @@ -71,7 +71,7 @@ impl PersistentGpuBuffer { let mut buffer_view = render_queue .write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size) .unwrap(); - data.write_bytes_le(metadata, &mut buffer_view); + data.write_bytes_le(metadata, &mut buffer_view, buffer_slice.start); } let queue_saturation = queue_count as f32 / self.write_queue.capacity() as f32; @@ -123,5 +123,10 @@ pub trait PersistentGpuBufferable { /// Convert `self` + `metadata` into bytes (little-endian), and write to the provided buffer slice. /// Any bytes not written to in the slice will be zeroed out when uploaded to the GPU. - fn write_bytes_le(&self, metadata: Self::Metadata, buffer_slice: &mut [u8]); + fn write_bytes_le( + &self, + metadata: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ); } diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs index 9c2667d3f3..210a52becd 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs @@ -1,9 +1,50 @@ -use super::{ - asset::{Meshlet, MeshletBoundingSpheres, MeshletSimplificationError}, - persistent_buffer::PersistentGpuBufferable, -}; +use crate::meshlet::asset::{BvhNode, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBufferable}; use alloc::sync::Arc; use bevy_math::Vec2; +use bevy_render::render_resource::BufferAddress; + +impl PersistentGpuBufferable for Arc<[BvhNode]> { + type Metadata = u32; + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le( + &self, + base_meshlet_index: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ) { + const SIZE: usize = size_of::(); + for (i, &node) in self.iter().enumerate() { + let bytes: [u8; SIZE] = + bytemuck::cast(node.offset_aabbs(base_meshlet_index, buffer_offset)); + buffer_slice[i * SIZE..(i + 1) * SIZE].copy_from_slice(&bytes); + } + } +} + +impl BvhNode { + fn offset_aabbs(mut self, base_meshlet_index: u32, buffer_offset: BufferAddress) -> Self { + let size = size_of::(); + let base_bvh_node_index = (buffer_offset / size as u64) as u32; + for i in 0..self.aabbs.len() { + self.aabbs[i].child_offset += if self.child_is_bvh_node(i) { + base_bvh_node_index + } else { + base_meshlet_index + }; + } + self + } + + fn child_is_bvh_node(&self, i: usize) -> bool { + self.child_counts[i] == u8::MAX + } +} impl PersistentGpuBufferable for Arc<[Meshlet]> { type Metadata = (u64, u64, u64); @@ -16,6 +57,7 @@ impl PersistentGpuBufferable for Arc<[Meshlet]> { &self, (vertex_position_offset, vertex_attribute_offset, index_offset): Self::Metadata, buffer_slice: &mut [u8], + _: BufferAddress, ) { let vertex_position_offset = (vertex_position_offset * 8) as u32; let vertex_attribute_offset = (vertex_attribute_offset as usize / size_of::()) as u32; @@ -37,6 +79,18 @@ impl PersistentGpuBufferable for Arc<[Meshlet]> { } } +impl PersistentGpuBufferable for Arc<[MeshletCullData]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} + impl PersistentGpuBufferable for Arc<[u8]> { type Metadata = (); @@ -44,7 +98,7 @@ impl PersistentGpuBufferable for Arc<[u8]> { self.len() } - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { buffer_slice.clone_from_slice(self); } } @@ -56,7 +110,7 @@ impl PersistentGpuBufferable for Arc<[u32]> { self.len() * size_of::() } - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); } } @@ -68,31 +122,7 @@ impl PersistentGpuBufferable for Arc<[Vec2]> { self.len() * size_of::() } - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { - buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); - } -} - -impl PersistentGpuBufferable for Arc<[MeshletBoundingSpheres]> { - type Metadata = (); - - fn size_in_bytes(&self) -> usize { - self.len() * size_of::() - } - - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { - buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); - } -} - -impl PersistentGpuBufferable for Arc<[MeshletSimplificationError]> { - type Metadata = (); - - fn size_in_bytes(&self) -> usize { - self.len() * size_of::() - } - - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); } } diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs index c25d896b8a..0fe9905d32 100644 --- a/crates/bevy_pbr/src/meshlet/pipelines.rs +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -1,37 +1,26 @@ use super::resource_manager::ResourceManager; -use bevy_asset::{weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, 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, + core_3d::CORE_3D_DEPTH_FORMAT, experimental::mip_generation::DownsampleDepthShader, + FullscreenShader, }; use bevy_ecs::{ resource::Resource, world::{FromWorld, World}, }; use bevy_render::render_resource::*; - -pub const MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE: Handle = - weak_handle!("a4bf48e4-5605-4d1c-987e-29c7b1ec95dc"); -pub const MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE: Handle = - weak_handle!("80ccea4a-8234-4ee0-af74-77b3cad503cf"); -pub const MESHLET_CULLING_SHADER_HANDLE: Handle = - weak_handle!("d71c5879-97fa-49d1-943e-ed9162fe8adb"); -pub const MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE: Handle = - weak_handle!("68cc6826-8321-43d1-93d5-4f61f0456c13"); -pub const MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE: Handle = - weak_handle!("4b4e3020-748f-4baf-b011-87d9d2a12796"); -pub const MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE: Handle = - weak_handle!("c218ce17-cf59-4268-8898-13ecf384f133"); -pub const MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE: Handle = - weak_handle!("f5b7edfc-2eac-4407-8f5c-1265d4d795c2"); +use bevy_utils::default; #[derive(Resource)] pub struct MeshletPipelines { - fill_cluster_buffers: CachedComputePipelineId, clear_visibility_buffer: CachedComputePipelineId, clear_visibility_buffer_shadow_view: CachedComputePipelineId, - cull_first: CachedComputePipelineId, - cull_second: CachedComputePipelineId, + first_instance_cull: CachedComputePipelineId, + second_instance_cull: CachedComputePipelineId, + first_bvh_cull: CachedComputePipelineId, + second_bvh_cull: CachedComputePipelineId, + first_meshlet_cull: CachedComputePipelineId, + second_meshlet_cull: CachedComputePipelineId, downsample_depth_first: CachedComputePipelineId, downsample_depth_second: CachedComputePipelineId, downsample_depth_first_shadow_view: CachedComputePipelineId, @@ -45,21 +34,35 @@ pub struct MeshletPipelines { resolve_depth_shadow_view: CachedRenderPipelineId, resolve_material_depth: CachedRenderPipelineId, remap_1d_to_2d_dispatch: Option, + fill_counts: CachedComputePipelineId, + pub(crate) meshlet_mesh_material: Handle, } impl FromWorld for MeshletPipelines { fn from_world(world: &mut World) -> Self { let resource_manager = world.resource::(); - let fill_cluster_buffers_bind_group_layout = resource_manager - .fill_cluster_buffers_bind_group_layout - .clone(); let clear_visibility_buffer_bind_group_layout = resource_manager .clear_visibility_buffer_bind_group_layout .clone(); let clear_visibility_buffer_shadow_view_bind_group_layout = resource_manager .clear_visibility_buffer_shadow_view_bind_group_layout .clone(); - let cull_layout = resource_manager.culling_bind_group_layout.clone(); + let first_instance_cull_bind_group_layout = resource_manager + .first_instance_cull_bind_group_layout + .clone(); + let second_instance_cull_bind_group_layout = resource_manager + .second_instance_cull_bind_group_layout + .clone(); + let first_bvh_cull_bind_group_layout = + resource_manager.first_bvh_cull_bind_group_layout.clone(); + let second_bvh_cull_bind_group_layout = + resource_manager.second_bvh_cull_bind_group_layout.clone(); + let first_meshlet_cull_bind_group_layout = resource_manager + .first_meshlet_cull_bind_group_layout + .clone(); + let second_meshlet_cull_bind_group_layout = resource_manager + .second_meshlet_cull_bind_group_layout + .clone(); let downsample_depth_layout = resource_manager.downsample_depth_bind_group_layout.clone(); let downsample_depth_shadow_view_layout = resource_manager .downsample_depth_shadow_view_bind_group_layout @@ -80,24 +83,27 @@ impl FromWorld for MeshletPipelines { let remap_1d_to_2d_dispatch_layout = resource_manager .remap_1d_to_2d_dispatch_bind_group_layout .clone(); + + let downsample_depth_shader = (*world.resource::()).clone(); + let vertex_state = world.resource::().to_vertex_state(); + let fill_counts_layout = resource_manager.fill_counts_bind_group_layout.clone(); + + let clear_visibility_buffer = load_embedded_asset!(world, "clear_visibility_buffer.wgsl"); + let cull_instances = load_embedded_asset!(world, "cull_instances.wgsl"); + let cull_bvh = load_embedded_asset!(world, "cull_bvh.wgsl"); + let cull_clusters = load_embedded_asset!(world, "cull_clusters.wgsl"); + let visibility_buffer_software_raster = + load_embedded_asset!(world, "visibility_buffer_software_raster.wgsl"); + let visibility_buffer_hardware_raster = + load_embedded_asset!(world, "visibility_buffer_hardware_raster.wgsl"); + let resolve_render_targets = load_embedded_asset!(world, "resolve_render_targets.wgsl"); + let remap_1d_to_2d_dispatch = load_embedded_asset!(world, "remap_1d_to_2d_dispatch.wgsl"); + let fill_counts = load_embedded_asset!(world, "fill_counts.wgsl"); + let meshlet_mesh_material = load_embedded_asset!(world, "meshlet_mesh_material.wgsl"); + let pipeline_cache = world.resource_mut::(); Self { - fill_cluster_buffers: pipeline_cache.queue_compute_pipeline( - ComputePipelineDescriptor { - label: Some("meshlet_fill_cluster_buffers_pipeline".into()), - layout: vec![fill_cluster_buffers_bind_group_layout], - push_constant_ranges: vec![PushConstantRange { - stages: ShaderStages::COMPUTE, - range: 0..4, - }], - shader: MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, - shader_defs: vec!["MESHLET_FILL_CLUSTER_BUFFERS_PASS".into()], - entry_point: "fill_cluster_buffers".into(), - zero_initialize_workgroup_memory: false, - }, - ), - clear_visibility_buffer: pipeline_cache.queue_compute_pipeline( ComputePipelineDescriptor { label: Some("meshlet_clear_visibility_buffer_pipeline".into()), @@ -106,10 +112,9 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..8, }], - shader: MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE, + shader: clear_visibility_buffer.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], - entry_point: "clear_visibility_buffer".into(), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -121,43 +126,101 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..8, }], - shader: MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "clear_visibility_buffer".into(), - zero_initialize_workgroup_memory: false, + shader: clear_visibility_buffer, + ..default() }, ), - cull_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("meshlet_culling_first_pipeline".into()), - layout: vec![cull_layout.clone()], + first_instance_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_instance_cull_pipeline".into()), + layout: vec![first_instance_cull_bind_group_layout.clone()], push_constant_ranges: vec![PushConstantRange { stages: ShaderStages::COMPUTE, - range: 0..8, + range: 0..4, }], - shader: MESHLET_CULLING_SHADER_HANDLE, + shader: cull_instances.clone(), shader_defs: vec![ - "MESHLET_CULLING_PASS".into(), + "MESHLET_INSTANCE_CULLING_PASS".into(), "MESHLET_FIRST_CULLING_PASS".into(), ], - entry_point: "cull_clusters".into(), - zero_initialize_workgroup_memory: false, + ..default() }), - cull_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("meshlet_culling_second_pipeline".into()), - layout: vec![cull_layout], + second_instance_cull: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_second_instance_cull_pipeline".into()), + layout: vec![second_instance_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_instances, + shader_defs: vec![ + "MESHLET_INSTANCE_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }, + ), + + first_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_bvh_cull_pipeline".into()), + layout: vec![first_bvh_cull_bind_group_layout.clone()], push_constant_ranges: vec![PushConstantRange { stages: ShaderStages::COMPUTE, range: 0..8, }], - shader: MESHLET_CULLING_SHADER_HANDLE, + shader: cull_bvh.clone(), shader_defs: vec![ - "MESHLET_CULLING_PASS".into(), + "MESHLET_BVH_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_bvh_cull_pipeline".into()), + layout: vec![second_bvh_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: cull_bvh, + shader_defs: vec![ + "MESHLET_BVH_CULLING_PASS".into(), "MESHLET_SECOND_CULLING_PASS".into(), ], - entry_point: "cull_clusters".into(), - zero_initialize_workgroup_memory: false, + ..default() + }), + + first_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_meshlet_cull_pipeline".into()), + layout: vec![first_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters.clone(), + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_meshlet_cull_pipeline".into()), + layout: vec![second_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters, + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() }), downsample_depth_first: pipeline_cache.queue_compute_pipeline( @@ -168,13 +231,13 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..4, }], - shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader: downsample_depth_shader.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), "MESHLET".into(), ], - entry_point: "downsample_depth_first".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("downsample_depth_first".into()), + ..default() }, ), @@ -186,13 +249,13 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..4, }], - shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader: downsample_depth_shader.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), "MESHLET".into(), ], - entry_point: "downsample_depth_second".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("downsample_depth_second".into()), + ..default() }, ), @@ -204,10 +267,10 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..4, }], - shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader: downsample_depth_shader.clone(), shader_defs: vec!["MESHLET".into()], - entry_point: "downsample_depth_first".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("downsample_depth_first".into()), + ..default() }, ), @@ -219,9 +282,9 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..4, }], - shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader: downsample_depth_shader, shader_defs: vec!["MESHLET".into()], - entry_point: "downsample_depth_second".into(), + entry_point: Some("downsample_depth_second".into()), zero_initialize_workgroup_memory: false, }, ), @@ -231,7 +294,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_visibility_buffer_software_raster_pipeline".into()), layout: vec![visibility_buffer_raster_layout.clone()], push_constant_ranges: vec![], - shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_software_raster.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), @@ -242,8 +305,7 @@ impl FromWorld for MeshletPipelines { } .into(), ], - entry_point: "rasterize_cluster".into(), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -254,7 +316,7 @@ impl FromWorld for MeshletPipelines { ), layout: vec![visibility_buffer_raster_shadow_view_layout.clone()], push_constant_ranges: vec![], - shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_software_raster, shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), if remap_1d_to_2d_dispatch_layout.is_some() { @@ -264,8 +326,7 @@ impl FromWorld for MeshletPipelines { } .into(), ], - entry_point: "rasterize_cluster".into(), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -278,39 +339,27 @@ impl FromWorld for MeshletPipelines { range: 0..4, }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), ], - entry_point: "vertex".into(), - buffers: vec![], + ..default() }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: Some(Face::Back), - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), ], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::R8Uint, blend: None, write_mask: ColorWrites::empty(), })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -325,33 +374,21 @@ impl FromWorld for MeshletPipelines { range: 0..4, }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "vertex".into(), - buffers: vec![], + ..default() }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: Some(Face::Back), - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::R8Uint, blend: None, write_mask: ColorWrites::empty(), })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -367,41 +404,27 @@ impl FromWorld for MeshletPipelines { range: 0..4, }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "vertex".into(), - buffers: vec![], + ..default() }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: Some(Face::Back), - unclipped_depth: true, - polygon_mode: PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster, shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::R8Uint, blend: None, write_mask: ColorWrites::empty(), })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }), resolve_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_layout], - push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), - primitive: PrimitiveState::default(), + vertex: vertex_state.clone(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -409,23 +432,20 @@ impl FromWorld for MeshletPipelines { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, + shader: resolve_render_targets.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], - entry_point: "resolve_depth".into(), - targets: vec![], + entry_point: Some("resolve_depth".into()), + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }), resolve_depth_shadow_view: pipeline_cache.queue_render_pipeline( RenderPipelineDescriptor { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_shadow_view_layout], - push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), - primitive: PrimitiveState::default(), + vertex: vertex_state.clone(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -433,14 +453,12 @@ impl FromWorld for MeshletPipelines { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "resolve_depth".into(), - targets: vec![], + shader: resolve_render_targets.clone(), + entry_point: Some("resolve_depth".into()), + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -448,8 +466,7 @@ impl FromWorld for MeshletPipelines { RenderPipelineDescriptor { 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, @@ -458,17 +475,29 @@ impl FromWorld for MeshletPipelines { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, + shader: resolve_render_targets, shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], - entry_point: "resolve_material_depth".into(), + entry_point: Some("resolve_material_depth".into()), targets: vec![], }), - zero_initialize_workgroup_memory: false, + ..default() }, ), + fill_counts: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_fill_counts_pipeline".into()), + layout: vec![fill_counts_layout], + shader: fill_counts, + shader_defs: vec![if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into()], + ..default() + }), + remap_1d_to_2d_dispatch: remap_1d_to_2d_dispatch_layout.map(|layout| { pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("meshlet_remap_1d_to_2d_dispatch_pipeline".into()), @@ -477,12 +506,12 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..4, }], - shader: MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "remap_dispatch".into(), - zero_initialize_workgroup_memory: false, + shader: remap_1d_to_2d_dispatch, + ..default() }) }), + + meshlet_mesh_material, } } } @@ -502,6 +531,9 @@ impl MeshletPipelines { &ComputePipeline, &ComputePipeline, &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, &RenderPipeline, &RenderPipeline, &RenderPipeline, @@ -509,15 +541,19 @@ impl MeshletPipelines { &RenderPipeline, &RenderPipeline, Option<&ComputePipeline>, + &ComputePipeline, )> { let pipeline_cache = world.get_resource::()?; let pipeline = world.get_resource::()?; Some(( - pipeline_cache.get_compute_pipeline(pipeline.fill_cluster_buffers)?, pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer)?, pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer_shadow_view)?, - pipeline_cache.get_compute_pipeline(pipeline.cull_first)?, - pipeline_cache.get_compute_pipeline(pipeline.cull_second)?, + pipeline_cache.get_compute_pipeline(pipeline.first_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_meshlet_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_meshlet_cull)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first_shadow_view)?, @@ -538,6 +574,7 @@ impl MeshletPipelines { Some(id) => Some(pipeline_cache.get_compute_pipeline(id)?), None => None, }, + pipeline_cache.get_compute_pipeline(pipeline.fill_counts)?, )) } } diff --git a/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl b/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl index fc98443634..b9970c42b4 100644 --- a/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl +++ b/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl @@ -13,11 +13,12 @@ var max_compute_workgroups_per_dimension: u32; @compute @workgroup_size(1, 1, 1) fn remap_dispatch() { - meshlet_software_raster_cluster_count = meshlet_software_raster_indirect_args.x; + let cluster_count = meshlet_software_raster_indirect_args.x; - if meshlet_software_raster_cluster_count > max_compute_workgroups_per_dimension { - let n = u32(ceil(sqrt(f32(meshlet_software_raster_cluster_count)))); + if cluster_count > max_compute_workgroups_per_dimension { + let n = u32(ceil(sqrt(f32(cluster_count)))); meshlet_software_raster_indirect_args.x = n; meshlet_software_raster_indirect_args.y = n; + meshlet_software_raster_cluster_count = cluster_count; } } diff --git a/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl b/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl index eaa4eed6c4..6fef0cc227 100644 --- a/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl +++ b/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl @@ -1,11 +1,12 @@ #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::meshlet_bindings::InstancedOffset #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT @group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; #else @group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; #endif -@group(0) @binding(1) var meshlet_cluster_instance_ids: array; // Per cluster +@group(0) @binding(1) var meshlet_raster_clusters: array; // Per cluster @group(0) @binding(2) var meshlet_instance_material_ids: array; // Per entity instance /// This pass writes out the depth texture. @@ -33,7 +34,7 @@ fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f3 if depth == 0lu { discard; } let cluster_id = u32(visibility) >> 7u; - let instance_id = meshlet_cluster_instance_ids[cluster_id]; + let instance_id = meshlet_raster_clusters[cluster_id].instance_id; let material_id = meshlet_instance_material_ids[instance_id]; return f32(material_id) / 65535.0; } diff --git a/crates/bevy_pbr/src/meshlet/resource_manager.rs b/crates/bevy_pbr/src/meshlet/resource_manager.rs index 9b45d7676a..dacab4afc4 100644 --- a/crates/bevy_pbr/src/meshlet/resource_manager.rs +++ b/crates/bevy_pbr/src/meshlet/resource_manager.rs @@ -1,6 +1,5 @@ use super::{instance_manager::InstanceManager, meshlet_mesh_manager::MeshletMeshManager}; use crate::ShadowView; -use alloc::sync::Arc; use bevy_core_pipeline::{ core_3d::Camera3d, experimental::mip_generation::{self, ViewDepthPyramid}, @@ -13,6 +12,7 @@ use bevy_ecs::{ resource::Resource, system::{Commands, Query, Res, ResMut}, }; +use bevy_image::ToExtents; use bevy_math::{UVec2, Vec4Swizzles}; use bevy_render::{ render_resource::*, @@ -21,25 +21,26 @@ use bevy_render::{ view::{ExtractedView, RenderLayers, ViewUniform, ViewUniforms}, }; use binding_types::*; -use core::{iter, sync::atomic::AtomicBool}; -use encase::internal::WriteInto; +use core::iter; /// Manages per-view and per-cluster GPU resources for [`super::MeshletPlugin`]. #[derive(Resource)] pub struct ResourceManager { /// Intermediate buffer of cluster IDs for use with rasterizing the visibility buffer visibility_buffer_raster_clusters: Buffer, + /// Intermediate buffer of previous counts of clusters in rasterizer buckets + pub visibility_buffer_raster_cluster_prev_counts: Buffer, /// Intermediate buffer of count of clusters to software rasterize software_raster_cluster_count: Buffer, - /// Rightmost slot index of [`Self::visibility_buffer_raster_clusters`] - raster_cluster_rightmost_slot: u32, + /// BVH traversal queues + bvh_traversal_queues: [Buffer; 2], + /// Cluster cull candidate queue + cluster_cull_candidate_queue: Buffer, + /// Rightmost slot index of [`Self::visibility_buffer_raster_clusters`], [`Self::bvh_traversal_queues`], and [`Self::cluster_cull_candidate_queue`] + cull_queue_rightmost_slot: u32, - /// Per-cluster instance ID - cluster_instance_ids: Option, - /// Per-cluster meshlet ID - cluster_meshlet_ids: Option, - /// Per-cluster bitmask of whether or not it's a candidate for the second raster pass - second_pass_candidates_buffer: Option, + /// Second pass instance candidates + second_pass_candidates: Option, /// Sampler for a depth pyramid depth_pyramid_sampler: Sampler, /// Dummy texture view for binding depth pyramids with less than the maximum amount of mips @@ -49,10 +50,14 @@ pub struct ResourceManager { previous_depth_pyramids: EntityHashMap, // Bind group layouts - pub fill_cluster_buffers_bind_group_layout: BindGroupLayout, pub clear_visibility_buffer_bind_group_layout: BindGroupLayout, pub clear_visibility_buffer_shadow_view_bind_group_layout: BindGroupLayout, - pub culling_bind_group_layout: BindGroupLayout, + pub first_instance_cull_bind_group_layout: BindGroupLayout, + pub second_instance_cull_bind_group_layout: BindGroupLayout, + pub first_bvh_cull_bind_group_layout: BindGroupLayout, + pub second_bvh_cull_bind_group_layout: BindGroupLayout, + pub first_meshlet_cull_bind_group_layout: BindGroupLayout, + pub second_meshlet_cull_bind_group_layout: BindGroupLayout, pub visibility_buffer_raster_bind_group_layout: BindGroupLayout, pub visibility_buffer_raster_shadow_view_bind_group_layout: BindGroupLayout, pub downsample_depth_bind_group_layout: BindGroupLayout, @@ -61,6 +66,7 @@ pub struct ResourceManager { pub resolve_depth_shadow_view_bind_group_layout: BindGroupLayout, pub resolve_material_depth_bind_group_layout: BindGroupLayout, pub material_shade_bind_group_layout: BindGroupLayout, + pub fill_counts_bind_group_layout: BindGroupLayout, pub remap_1d_to_2d_dispatch_bind_group_layout: Option, } @@ -68,25 +74,53 @@ impl ResourceManager { pub fn new(cluster_buffer_slots: u32, render_device: &RenderDevice) -> Self { let needs_dispatch_remap = cluster_buffer_slots > render_device.limits().max_compute_workgroups_per_dimension; + // The IDs are a (u32, u32) of instance and index. + let cull_queue_size = 2 * cluster_buffer_slots as u64 * size_of::() as u64; Self { visibility_buffer_raster_clusters: render_device.create_buffer(&BufferDescriptor { label: Some("meshlet_visibility_buffer_raster_clusters"), - size: cluster_buffer_slots as u64 * size_of::() as u64, + size: cull_queue_size, usage: BufferUsages::STORAGE, mapped_at_creation: false, }), + visibility_buffer_raster_cluster_prev_counts: render_device.create_buffer( + &BufferDescriptor { + label: Some("meshlet_visibility_buffer_raster_cluster_prev_counts"), + size: size_of::() as u64 * 2, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }, + ), software_raster_cluster_count: render_device.create_buffer(&BufferDescriptor { label: Some("meshlet_software_raster_cluster_count"), size: size_of::() as u64, usage: BufferUsages::STORAGE, mapped_at_creation: false, }), - raster_cluster_rightmost_slot: cluster_buffer_slots - 1, + bvh_traversal_queues: [ + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_0"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_1"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + ], + cluster_cull_candidate_queue: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_cluster_cull_candidate_queue"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + cull_queue_rightmost_slot: cluster_buffer_slots - 1, - cluster_instance_ids: None, - cluster_meshlet_ids: None, - second_pass_candidates_buffer: None, + second_pass_candidates: None, depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor { label: Some("meshlet_depth_pyramid_sampler"), ..SamplerDescriptor::default() @@ -100,19 +134,6 @@ impl ResourceManager { previous_depth_pyramids: EntityHashMap::default(), // TODO: Buffer min sizes - fill_cluster_buffers_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_fill_cluster_buffers_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - ), - ), - ), clear_visibility_buffer_bind_group_layout: render_device.create_bind_group_layout( "meshlet_clear_visibility_buffer_bind_group_layout", &BindGroupLayoutEntries::single( @@ -128,24 +149,131 @@ impl ResourceManager { texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), ), ), - culling_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_culling_bind_group_layout", + first_instance_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_instance_culling_bind_group_layout", &BindGroupLayoutEntries::sequential( ShaderStages::COMPUTE, ( - 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), - storage_buffer_read_only_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), texture_2d(TextureSampleType::Float { filterable: false }), uniform_buffer::(true), uniform_buffer::(true), + 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_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_instance_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_instance_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + 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_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + first_bvh_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_bvh_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + first_meshlet_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_meshlet_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), ), ), ), @@ -206,7 +334,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), @@ -215,7 +343,6 @@ impl ResourceManager { 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), texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::Atomic), uniform_buffer::(true), ), @@ -225,7 +352,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), @@ -234,7 +361,6 @@ impl ResourceManager { 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), texture_storage_2d( TextureFormat::R32Uint, StorageTextureAccess::Atomic, @@ -281,10 +407,35 @@ impl ResourceManager { 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), ), ), ), + fill_counts_bind_group_layout: if needs_dispatch_remap { + render_device.create_bind_group_layout( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + } else { + render_device.create_bind_group_layout( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + }, remap_1d_to_2d_dispatch_bind_group_layout: needs_dispatch_remap.then(|| { render_device.create_bind_group_layout( "meshlet_remap_1d_to_2d_dispatch_bind_group_layout", @@ -306,57 +457,56 @@ impl ResourceManager { #[derive(Component)] pub struct MeshletViewResources { pub scene_instance_count: u32, - pub scene_cluster_count: u32, - pub second_pass_candidates_buffer: Buffer, + pub rightmost_slot: u32, + pub max_bvh_depth: u32, instance_visibility: Buffer, pub dummy_render_target: CachedTexture, pub visibility_buffer: CachedTexture, - pub visibility_buffer_software_raster_indirect_args_first: Buffer, - pub visibility_buffer_software_raster_indirect_args_second: Buffer, - pub visibility_buffer_hardware_raster_indirect_args_first: Buffer, - pub visibility_buffer_hardware_raster_indirect_args_second: Buffer, + pub second_pass_count: Buffer, + pub second_pass_dispatch: Buffer, + pub second_pass_candidates: Buffer, + pub first_bvh_cull_count_front: Buffer, + pub first_bvh_cull_dispatch_front: Buffer, + pub first_bvh_cull_count_back: Buffer, + pub first_bvh_cull_dispatch_back: Buffer, + pub first_bvh_cull_queue: Buffer, + pub second_bvh_cull_count_front: Buffer, + pub second_bvh_cull_dispatch_front: Buffer, + pub second_bvh_cull_count_back: Buffer, + pub second_bvh_cull_dispatch_back: Buffer, + pub second_bvh_cull_queue: Buffer, + pub front_meshlet_cull_count: Buffer, + pub front_meshlet_cull_dispatch: Buffer, + pub back_meshlet_cull_count: Buffer, + pub back_meshlet_cull_dispatch: Buffer, + pub meshlet_cull_queue: Buffer, + pub visibility_buffer_software_raster_indirect_args: Buffer, + pub visibility_buffer_hardware_raster_indirect_args: Buffer, pub depth_pyramid: ViewDepthPyramid, previous_depth_pyramid: TextureView, pub material_depth: Option, pub view_size: UVec2, - pub raster_cluster_rightmost_slot: u32, not_shadow_view: bool, } #[derive(Component)] pub struct MeshletViewBindGroups { - pub first_node: Arc, - pub fill_cluster_buffers: BindGroup, pub clear_visibility_buffer: BindGroup, - pub culling_first: BindGroup, - pub culling_second: BindGroup, + pub first_instance_cull: BindGroup, + pub second_instance_cull: BindGroup, + pub first_bvh_cull_ping: BindGroup, + pub first_bvh_cull_pong: BindGroup, + pub second_bvh_cull_ping: BindGroup, + pub second_bvh_cull_pong: BindGroup, + pub first_meshlet_cull: BindGroup, + pub second_meshlet_cull: BindGroup, pub downsample_depth: BindGroup, pub visibility_buffer_raster: BindGroup, pub resolve_depth: BindGroup, pub resolve_material_depth: Option, pub material_shade: Option, - pub remap_1d_to_2d_dispatch: Option<(BindGroup, BindGroup)>, -} - -// TODO: Try using Queue::write_buffer_with() in queue_meshlet_mesh_upload() to reduce copies -fn upload_storage_buffer( - buffer: &mut StorageBuffer>, - render_device: &RenderDevice, - render_queue: &RenderQueue, -) where - Vec: WriteInto, -{ - let inner = buffer.buffer(); - let capacity = inner.map_or(0, |b| b.size()); - let size = buffer.get().size().get() as BufferAddress; - - if capacity >= size { - let inner = inner.unwrap(); - let bytes = bytemuck::must_cast_slice(buffer.get().as_slice()); - render_queue.write_buffer(inner, 0, bytes); - } else { - buffer.write_buffer(render_device, render_queue); - } + pub remap_1d_to_2d_dispatch: Option, + pub fill_counts: BindGroup, } // TODO: Cache things per-view and skip running this system / optimize this system @@ -374,7 +524,7 @@ pub fn prepare_meshlet_per_frame_resources( render_device: Res, mut commands: Commands, ) { - if instance_manager.scene_cluster_count == 0 { + if instance_manager.scene_instance_count == 0 { return; } @@ -384,41 +534,22 @@ pub fn prepare_meshlet_per_frame_resources( instance_manager .instance_uniforms .write_buffer(&render_device, &render_queue); - upload_storage_buffer( - &mut instance_manager.instance_material_ids, - &render_device, - &render_queue, - ); - upload_storage_buffer( - &mut instance_manager.instance_meshlet_counts, - &render_device, - &render_queue, - ); - upload_storage_buffer( - &mut instance_manager.instance_meshlet_slice_starts, - &render_device, - &render_queue, - ); + instance_manager + .instance_aabbs + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_material_ids + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_bvh_root_nodes + .write_buffer(&render_device, &render_queue); - let needed_buffer_size = 4 * instance_manager.scene_cluster_count as u64; - match &mut resource_manager.cluster_instance_ids { + let needed_buffer_size = 4 * instance_manager.scene_instance_count as u64; + let second_pass_candidates = match &mut resource_manager.second_pass_candidates { Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), slot => { let buffer = render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_cluster_instance_ids"), - size: needed_buffer_size, - usage: BufferUsages::STORAGE, - mapped_at_creation: false, - }); - *slot = Some(buffer.clone()); - buffer - } - }; - match &mut resource_manager.cluster_meshlet_ids { - Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), - slot => { - let buffer = render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_cluster_meshlet_ids"), + label: Some("meshlet_second_pass_candidates"), size: needed_buffer_size, usage: BufferUsages::STORAGE, mapped_at_creation: false, @@ -428,8 +559,6 @@ pub fn prepare_meshlet_per_frame_resources( } }; - let needed_buffer_size = - instance_manager.scene_cluster_count.div_ceil(u32::BITS) as u64 * size_of::() as u64; for (view_entity, view, render_layers, (_, shadow_view)) in &views { let not_shadow_view = shadow_view.is_none(); @@ -460,34 +589,15 @@ pub fn prepare_meshlet_per_frame_resources( vec[index] |= 1 << bit; } } - upload_storage_buffer(instance_visibility, &render_device, &render_queue); + instance_visibility.write_buffer(&render_device, &render_queue); let instance_visibility = instance_visibility.buffer().unwrap().clone(); - let second_pass_candidates_buffer = - match &mut resource_manager.second_pass_candidates_buffer { - Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), - slot => { - let buffer = render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_second_pass_candidates"), - size: needed_buffer_size, - usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - *slot = Some(buffer.clone()); - buffer - } - }; - // TODO: Remove this once wgpu allows render passes with no attachments let dummy_render_target = texture_cache.get( &render_device, TextureDescriptor { label: Some("meshlet_dummy_render_target"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, + size: view.viewport.zw().to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -501,11 +611,7 @@ pub fn prepare_meshlet_per_frame_resources( &render_device, TextureDescriptor { label: Some("meshlet_visibility_buffer"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, + size: view.viewport.zw().to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -519,34 +625,102 @@ pub fn prepare_meshlet_per_frame_resources( }, ); - let visibility_buffer_software_raster_indirect_args_first = render_device - .create_buffer_with_data(&BufferInitDescriptor { - label: Some("meshlet_visibility_buffer_software_raster_indirect_args_first"), + let second_pass_count = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let second_pass_dispatch = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let first_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let first_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let second_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let second_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let front_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let front_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_dispatch"), contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, }); - let visibility_buffer_software_raster_indirect_args_second = render_device - .create_buffer_with_data(&BufferInitDescriptor { - label: Some("visibility_buffer_software_raster_indirect_args_second"), + let back_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let back_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_dispatch"), contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, }); - let visibility_buffer_hardware_raster_indirect_args_first = render_device + let visibility_buffer_software_raster_indirect_args = render_device .create_buffer_with_data(&BufferInitDescriptor { - label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args_first"), - contents: DrawIndirectArgs { - vertex_count: 128 * 3, - instance_count: 0, - first_vertex: 0, - first_instance: 0, - } - .as_bytes(), + label: Some("meshlet_visibility_buffer_software_raster_indirect_args"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, }); - let visibility_buffer_hardware_raster_indirect_args_second = render_device + + let visibility_buffer_hardware_raster_indirect_args = render_device .create_buffer_with_data(&BufferInitDescriptor { - label: Some("visibility_buffer_hardware_raster_indirect_args_second"), + label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args"), contents: DrawIndirectArgs { vertex_count: 128 * 3, instance_count: 0, @@ -577,11 +751,7 @@ pub fn prepare_meshlet_per_frame_resources( let material_depth = TextureDescriptor { label: Some("meshlet_material_depth"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, + size: view.viewport.zw().to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -592,21 +762,36 @@ pub fn prepare_meshlet_per_frame_resources( commands.entity(view_entity).insert(MeshletViewResources { scene_instance_count: instance_manager.scene_instance_count, - scene_cluster_count: instance_manager.scene_cluster_count, - second_pass_candidates_buffer, + rightmost_slot: resource_manager.cull_queue_rightmost_slot, + max_bvh_depth: instance_manager.max_bvh_depth, instance_visibility, dummy_render_target, visibility_buffer, - visibility_buffer_software_raster_indirect_args_first, - visibility_buffer_software_raster_indirect_args_second, - visibility_buffer_hardware_raster_indirect_args_first, - visibility_buffer_hardware_raster_indirect_args_second, + second_pass_count, + second_pass_dispatch, + second_pass_candidates: second_pass_candidates.clone(), + first_bvh_cull_count_front, + first_bvh_cull_dispatch_front, + first_bvh_cull_count_back, + first_bvh_cull_dispatch_back, + first_bvh_cull_queue: resource_manager.bvh_traversal_queues[0].clone(), + second_bvh_cull_count_front, + second_bvh_cull_dispatch_front, + second_bvh_cull_count_back, + second_bvh_cull_dispatch_back, + second_bvh_cull_queue: resource_manager.bvh_traversal_queues[1].clone(), + front_meshlet_cull_count, + front_meshlet_cull_dispatch, + back_meshlet_cull_count, + back_meshlet_cull_dispatch, + meshlet_cull_queue: resource_manager.cluster_cull_candidate_queue.clone(), + visibility_buffer_software_raster_indirect_args, + visibility_buffer_hardware_raster_indirect_args, depth_pyramid, previous_depth_pyramid, material_depth: not_shadow_view .then(|| texture_cache.get(&render_device, material_depth)), view_size: view.viewport.zw(), - raster_cluster_rightmost_slot: resource_manager.raster_cluster_rightmost_slot, not_shadow_view, }); } @@ -622,49 +807,15 @@ pub fn prepare_meshlet_view_bind_groups( render_device: Res, mut commands: Commands, ) { - let ( - Some(cluster_instance_ids), - Some(cluster_meshlet_ids), - Some(view_uniforms), - Some(previous_view_uniforms), - ) = ( - resource_manager.cluster_instance_ids.as_ref(), - resource_manager.cluster_meshlet_ids.as_ref(), + let (Some(view_uniforms), Some(previous_view_uniforms)) = ( view_uniforms.uniforms.binding(), previous_view_uniforms.uniforms.binding(), - ) - else { + ) else { return; }; - let first_node = Arc::new(AtomicBool::new(true)); - - let fill_cluster_buffers_global_cluster_count = - render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_fill_cluster_buffers_global_cluster_count"), - size: 4, - usage: BufferUsages::STORAGE, - mapped_at_creation: false, - }); - // TODO: Some of these bind groups can be reused across multiple views for (view_entity, view_resources) in &views { - let entries = BindGroupEntries::sequential(( - instance_manager.instance_meshlet_counts.binding().unwrap(), - instance_manager - .instance_meshlet_slice_starts - .binding() - .unwrap(), - cluster_instance_ids.as_entire_binding(), - cluster_meshlet_ids.as_entire_binding(), - fill_cluster_buffers_global_cluster_count.as_entire_binding(), - )); - let fill_cluster_buffers = render_device.create_bind_group( - "meshlet_fill_cluster_buffers", - &resource_manager.fill_cluster_buffers_bind_group_layout, - &entries, - ); - let clear_visibility_buffer = render_device.create_bind_group( "meshlet_clear_visibility_buffer_bind_group", if view_resources.not_shadow_view { @@ -675,62 +826,241 @@ pub fn prepare_meshlet_view_bind_groups( &BindGroupEntries::single(&view_resources.visibility_buffer.default_view), ); - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlet_bounding_spheres.binding(), - meshlet_mesh_manager.meshlet_simplification_errors.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - view_resources.instance_visibility.as_entire_binding(), - view_resources - .second_pass_candidates_buffer - .as_entire_binding(), - view_resources - .visibility_buffer_software_raster_indirect_args_first - .as_entire_binding(), - view_resources - .visibility_buffer_hardware_raster_indirect_args_first - .as_entire_binding(), - resource_manager - .visibility_buffer_raster_clusters - .as_entire_binding(), - &view_resources.previous_depth_pyramid, - view_uniforms.clone(), - previous_view_uniforms.clone(), - )); - let culling_first = render_device.create_bind_group( - "meshlet_culling_first_bind_group", - &resource_manager.culling_bind_group_layout, - &entries, + let first_instance_cull = render_device.create_bind_group( + "meshlet_first_instance_cull_bind_group", + &resource_manager.first_instance_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_dispatch.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), ); - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlet_bounding_spheres.binding(), - meshlet_mesh_manager.meshlet_simplification_errors.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - view_resources.instance_visibility.as_entire_binding(), - view_resources - .second_pass_candidates_buffer - .as_entire_binding(), - view_resources - .visibility_buffer_software_raster_indirect_args_second - .as_entire_binding(), - view_resources - .visibility_buffer_hardware_raster_indirect_args_second - .as_entire_binding(), - resource_manager - .visibility_buffer_raster_clusters - .as_entire_binding(), - &view_resources.depth_pyramid.all_mips, - view_uniforms.clone(), - previous_view_uniforms.clone(), - )); - let culling_second = render_device.create_bind_group( - "meshlet_culling_second_bind_group", - &resource_manager.culling_bind_group_layout, - &entries, + let second_instance_cull = render_device.create_bind_group( + "meshlet_second_instance_cull_bind_group", + &resource_manager.second_instance_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), + ); + + let first_bvh_cull_ping = render_device.create_bind_group( + "meshlet_first_bvh_cull_ping_bind_group", + &resource_manager.first_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let first_bvh_cull_pong = render_device.create_bind_group( + "meshlet_first_bvh_cull_pong_bind_group", + &resource_manager.first_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_ping = render_device.create_bind_group( + "meshlet_second_bvh_cull_ping_bind_group", + &resource_manager.second_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_pong = render_device.create_bind_group( + "meshlet_second_bvh_cull_pong_bind_group", + &resource_manager.second_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let first_meshlet_cull = render_device.create_bind_group( + "meshlet_first_meshlet_cull_bind_group", + &resource_manager.first_meshlet_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_meshlet_cull = render_device.create_bind_group( + "meshlet_second_meshlet_cull_bind_group", + &resource_manager.second_meshlet_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), ); let downsample_depth = view_resources.depth_pyramid.create_bind_group( @@ -745,22 +1075,6 @@ pub fn prepare_meshlet_view_bind_groups( &resource_manager.depth_pyramid_sampler, ); - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlets.binding(), - meshlet_mesh_manager.indices.binding(), - meshlet_mesh_manager.vertex_positions.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - resource_manager - .visibility_buffer_raster_clusters - .as_entire_binding(), - resource_manager - .software_raster_cluster_count - .as_entire_binding(), - &view_resources.visibility_buffer.default_view, - view_uniforms.clone(), - )); let visibility_buffer_raster = render_device.create_bind_group( "meshlet_visibility_raster_buffer_bind_group", if view_resources.not_shadow_view { @@ -768,7 +1082,23 @@ pub fn prepare_meshlet_view_bind_groups( } else { &resource_manager.visibility_buffer_raster_shadow_view_bind_group_layout }, - &entries, + &BindGroupEntries::sequential(( + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + &view_resources.visibility_buffer.default_view, + view_uniforms.clone(), + )), ); let resolve_depth = render_device.create_bind_group( @@ -782,34 +1112,35 @@ pub fn prepare_meshlet_view_bind_groups( ); let resolve_material_depth = view_resources.material_depth.as_ref().map(|_| { - let entries = BindGroupEntries::sequential(( - &view_resources.visibility_buffer.default_view, - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_material_ids.binding().unwrap(), - )); render_device.create_bind_group( "meshlet_resolve_material_depth_bind_group", &resource_manager.resolve_material_depth_bind_group_layout, - &entries, + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + instance_manager.instance_material_ids.binding().unwrap(), + )), ) }); let material_shade = view_resources.material_depth.as_ref().map(|_| { - let entries = BindGroupEntries::sequential(( - &view_resources.visibility_buffer.default_view, - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlets.binding(), - meshlet_mesh_manager.indices.binding(), - meshlet_mesh_manager.vertex_positions.binding(), - meshlet_mesh_manager.vertex_normals.binding(), - meshlet_mesh_manager.vertex_uvs.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - )); render_device.create_bind_group( "meshlet_mesh_material_shade_bind_group", &resource_manager.material_shade_bind_group_layout, - &entries, + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + meshlet_mesh_manager.vertex_normals.binding(), + meshlet_mesh_manager.vertex_uvs.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + )), ) }); @@ -817,46 +1148,77 @@ pub fn prepare_meshlet_view_bind_groups( .remap_1d_to_2d_dispatch_bind_group_layout .as_ref() .map(|layout| { - ( - render_device.create_bind_group( - "meshlet_remap_1d_to_2d_dispatch_first_bind_group", - layout, - &BindGroupEntries::sequential(( - view_resources - .visibility_buffer_software_raster_indirect_args_first - .as_entire_binding(), - resource_manager - .software_raster_cluster_count - .as_entire_binding(), - )), - ), - render_device.create_bind_group( - "meshlet_remap_1d_to_2d_dispatch_second_bind_group", - layout, - &BindGroupEntries::sequential(( - view_resources - .visibility_buffer_software_raster_indirect_args_second - .as_entire_binding(), - resource_manager - .software_raster_cluster_count - .as_entire_binding(), - )), - ), + render_device.create_bind_group( + "meshlet_remap_1d_to_2d_dispatch_bind_group", + layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), ) }); + let fill_counts = if resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .is_some() + { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &resource_manager.fill_counts_bind_group_layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), + ) + } else { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &resource_manager.fill_counts_bind_group_layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + )), + ) + }; + commands.entity(view_entity).insert(MeshletViewBindGroups { - first_node: Arc::clone(&first_node), - fill_cluster_buffers, clear_visibility_buffer, - culling_first, - culling_second, + first_instance_cull, + second_instance_cull, + first_bvh_cull_ping, + first_bvh_cull_pong, + second_bvh_cull_ping, + second_bvh_cull_pong, + first_meshlet_cull, + second_meshlet_cull, downsample_depth, visibility_buffer_raster, resolve_depth, resolve_material_depth, material_shade, remap_1d_to_2d_dispatch, + fill_counts, }); } } diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl index 3525d38e6d..2a251443fb 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl @@ -5,6 +5,7 @@ meshlet_cluster_instance_ids, meshlet_instance_uniforms, meshlet_raster_clusters, + meshlet_previous_raster_counts, meshlet_visibility_buffer, view, get_meshlet_triangle_count, @@ -27,17 +28,17 @@ struct VertexOutput { @vertex fn vertex(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) vertex_index: u32) -> VertexOutput { - let cluster_id = meshlet_raster_clusters[meshlet_raster_cluster_rightmost_slot - instance_index]; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; - var meshlet = meshlets[meshlet_id]; + let cluster_in_draw = meshlet_previous_raster_counts[1] + instance_index; + let cluster_id = meshlet_raster_cluster_rightmost_slot - cluster_in_draw; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; let triangle_id = vertex_index / 3u; if triangle_id >= get_meshlet_triangle_count(&meshlet) { return dummy_vertex(); } - let index_id = (triangle_id * 3u) + (vertex_index % 3u); + let index_id = vertex_index; let vertex_id = get_meshlet_vertex_id(meshlet.start_index_id + index_id); - let instance_id = meshlet_cluster_instance_ids[cluster_id]; - let instance_uniform = meshlet_instance_uniforms[instance_id]; + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id); let world_from_local = affine3_to_square(instance_uniform.world_from_local); diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs index 20054d2d2f..160097fc50 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs @@ -2,14 +2,16 @@ use super::{ pipelines::MeshletPipelines, resource_manager::{MeshletViewBindGroups, MeshletViewResources}, }; -use crate::{LightEntity, ShadowView, ViewLightEntities}; +use crate::{ + meshlet::resource_manager::ResourceManager, LightEntity, ShadowView, ViewLightEntities, +}; use bevy_color::LinearRgba; use bevy_core_pipeline::prepass::PreviousViewUniformOffset; use bevy_ecs::{ query::QueryState, world::{FromWorld, World}, }; -use bevy_math::{ops, UVec2}; +use bevy_math::UVec2; use bevy_render::{ camera::ExtractedCamera, render_graph::{Node, NodeRunError, RenderGraphContext}, @@ -17,7 +19,6 @@ use bevy_render::{ renderer::RenderContext, view::{ViewDepthTexture, ViewUniformOffset}, }; -use core::sync::atomic::Ordering; /// Rasterize meshlets into a depth buffer, and optional visibility buffer + material depth buffer for shading passes. pub struct MeshletVisibilityBufferRasterPassNode { @@ -76,11 +77,14 @@ impl Node for MeshletVisibilityBufferRasterPassNode { }; let Some(( - fill_cluster_buffers_pipeline, clear_visibility_buffer_pipeline, clear_visibility_buffer_shadow_view_pipeline, - culling_first_pipeline, - culling_second_pipeline, + first_instance_cull_pipeline, + second_instance_cull_pipeline, + first_bvh_cull_pipeline, + second_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + second_meshlet_cull_pipeline, downsample_depth_first_pipeline, downsample_depth_second_pipeline, downsample_depth_first_shadow_view_pipeline, @@ -94,69 +98,60 @@ impl Node for MeshletVisibilityBufferRasterPassNode { resolve_depth_shadow_view_pipeline, resolve_material_depth_pipeline, remap_1d_to_2d_dispatch_pipeline, + fill_counts_pipeline, )) = MeshletPipelines::get(world) else { return Ok(()); }; - let first_node = meshlet_view_bind_groups - .first_node - .fetch_and(false, Ordering::SeqCst); - - let div_ceil = meshlet_view_resources.scene_cluster_count.div_ceil(128); - let thread_per_cluster_workgroups = ops::cbrt(div_ceil as f32).ceil() as u32; - render_context .command_encoder() .push_debug_group("meshlet_visibility_buffer_raster"); - if first_node { - fill_cluster_buffers_pass( - render_context, - &meshlet_view_bind_groups.fill_cluster_buffers, - fill_cluster_buffers_pipeline, - meshlet_view_resources.scene_instance_count, - ); - } + + let resource_manager = world.get_resource::().unwrap(); + render_context.command_encoder().clear_buffer( + &resource_manager.visibility_buffer_raster_cluster_prev_counts, + 0, + None, + ); + clear_visibility_buffer_pass( render_context, &meshlet_view_bind_groups.clear_visibility_buffer, clear_visibility_buffer_pipeline, meshlet_view_resources.view_size, ); - render_context.command_encoder().clear_buffer( - &meshlet_view_resources.second_pass_candidates_buffer, - 0, - None, - ); - cull_pass( - "culling_first", + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( render_context, - &meshlet_view_bind_groups.culling_first, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_first_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(bg1, _)| bg1), + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( true, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_pipeline, visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, Some(camera), - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + meshlet_view_resources.depth_pyramid.downsample_depth( "downsample_depth", render_context, @@ -165,35 +160,37 @@ impl Node for MeshletVisibilityBufferRasterPassNode { downsample_depth_first_pipeline, downsample_depth_second_pipeline, ); - cull_pass( - "culling_second", + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( render_context, - &meshlet_view_bind_groups.culling_second, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_second_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(_, bg2)| bg2), + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( false, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_pipeline, visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, Some(camera), - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + resolve_depth( render_context, view_depth.get_attachment(StoreOp::Store), @@ -248,40 +245,37 @@ impl Node for MeshletVisibilityBufferRasterPassNode { clear_visibility_buffer_shadow_view_pipeline, meshlet_view_resources.view_size, ); - render_context.command_encoder().clear_buffer( - &meshlet_view_resources.second_pass_candidates_buffer, - 0, - None, - ); - cull_pass( - "culling_first", + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( render_context, - &meshlet_view_bind_groups.culling_first, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_first_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(bg1, _)| bg1), + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( true, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_shadow_view_pipeline, shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, None, - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + meshlet_view_resources.depth_pyramid.downsample_depth( "downsample_depth", render_context, @@ -290,35 +284,37 @@ impl Node for MeshletVisibilityBufferRasterPassNode { downsample_depth_first_shadow_view_pipeline, downsample_depth_second_shadow_view_pipeline, ); - cull_pass( - "culling_second", + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( render_context, - &meshlet_view_bind_groups.culling_second, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_second_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(_, bg2)| bg2), + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( false, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_shadow_view_pipeline, shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, None, - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + resolve_depth( render_context, shadow_view.depth_attachment.get_attachment(StoreOp::Store), @@ -341,39 +337,6 @@ impl Node for MeshletVisibilityBufferRasterPassNode { } } -fn fill_cluster_buffers_pass( - render_context: &mut RenderContext, - fill_cluster_buffers_bind_group: &BindGroup, - fill_cluster_buffers_pass_pipeline: &ComputePipeline, - scene_instance_count: u32, -) { - let mut fill_cluster_buffers_pass_workgroups_x = scene_instance_count; - let mut fill_cluster_buffers_pass_workgroups_y = 1; - if scene_instance_count - > render_context - .render_device() - .limits() - .max_compute_workgroups_per_dimension - { - fill_cluster_buffers_pass_workgroups_x = (scene_instance_count as f32).sqrt().ceil() as u32; - fill_cluster_buffers_pass_workgroups_y = fill_cluster_buffers_pass_workgroups_x; - } - - let command_encoder = render_context.command_encoder(); - let mut fill_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { - label: Some("fill_cluster_buffers"), - timestamp_writes: None, - }); - fill_pass.set_pipeline(fill_cluster_buffers_pass_pipeline); - fill_pass.set_push_constants(0, &scene_instance_count.to_le_bytes()); - fill_pass.set_bind_group(0, fill_cluster_buffers_bind_group, &[]); - fill_pass.dispatch_workgroups( - fill_cluster_buffers_pass_workgroups_x, - fill_cluster_buffers_pass_workgroups_y, - 1, - ); -} - // TODO: Replace this with vkCmdClearColorImage once wgpu supports it fn clear_visibility_buffer_pass( render_context: &mut RenderContext, @@ -397,82 +360,231 @@ fn clear_visibility_buffer_pass( ); } -fn cull_pass( - label: &'static str, +fn first_cull( render_context: &mut RenderContext, - culling_bind_group: &BindGroup, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, view_offset: &ViewUniformOffset, previous_view_offset: &PreviousViewUniformOffset, - culling_pipeline: &ComputePipeline, - culling_workgroups: u32, - scene_cluster_count: u32, - raster_cluster_rightmost_slot: u32, - remap_1d_to_2d_dispatch_bind_group: Option<&BindGroup>, - remap_1d_to_2d_dispatch_pipeline: Option<&ComputePipeline>, + first_instance_cull_pipeline: &ComputePipeline, + first_bvh_cull_pipeline: &ComputePipeline, + first_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, ) { - let max_compute_workgroups_per_dimension = render_context - .render_device() - .limits() - .max_compute_workgroups_per_dimension; + let workgroups = meshlet_view_resources.scene_instance_count.div_ceil(128); + cull_pass( + "meshlet_first_instance_cull", + render_context, + &meshlet_view_bind_groups.first_instance_cull, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups(workgroups, 1, 1); + render_context + .command_encoder() + .push_debug_group("meshlet_first_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_first_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.first_bvh_cull_ping + } else { + &meshlet_view_bind_groups.first_bvh_cull_pong + }, + view_offset, + previous_view_offset, + first_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_count_front + } else { + &meshlet_view_resources.first_bvh_cull_count_back + }, + 0, + Some(4), + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + Some(4), + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_first_meshlet_cull", + render_context, + &meshlet_view_bind_groups.first_meshlet_cull, + view_offset, + previous_view_offset, + first_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.front_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn second_cull( + render_context: &mut RenderContext, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, + view_offset: &ViewUniformOffset, + previous_view_offset: &PreviousViewUniformOffset, + second_instance_cull_pipeline: &ComputePipeline, + second_bvh_cull_pipeline: &ComputePipeline, + second_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, +) { + cull_pass( + "meshlet_second_instance_cull", + render_context, + &meshlet_view_bind_groups.second_instance_cull, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups_indirect(&meshlet_view_resources.second_pass_dispatch, 0); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_second_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.second_bvh_cull_ping + } else { + &meshlet_view_bind_groups.second_bvh_cull_pong + }, + view_offset, + previous_view_offset, + second_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.second_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.second_bvh_cull_dispatch_back + }, + 0, + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_second_meshlet_cull", + render_context, + &meshlet_view_bind_groups.second_meshlet_cull, + view_offset, + previous_view_offset, + second_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.back_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn cull_pass<'a>( + label: &'static str, + render_context: &'a mut RenderContext, + bind_group: &'a BindGroup, + view_offset: &'a ViewUniformOffset, + previous_view_offset: &'a PreviousViewUniformOffset, + pipeline: &'a ComputePipeline, + push_constants: &[u32], +) -> ComputePass<'a> { let command_encoder = render_context.command_encoder(); - let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { label: Some(label), timestamp_writes: None, }); - cull_pass.set_pipeline(culling_pipeline); - cull_pass.set_push_constants( + pass.set_pipeline(pipeline); + pass.set_bind_group( 0, - bytemuck::cast_slice(&[scene_cluster_count, raster_cluster_rightmost_slot]), - ); - cull_pass.set_bind_group( - 0, - culling_bind_group, + bind_group, &[view_offset.offset, previous_view_offset.offset], ); - cull_pass.dispatch_workgroups(culling_workgroups, culling_workgroups, culling_workgroups); + pass.set_push_constants(0, bytemuck::cast_slice(push_constants)); + pass +} - if let (Some(remap_1d_to_2d_dispatch_pipeline), Some(remap_1d_to_2d_dispatch_bind_group)) = ( - remap_1d_to_2d_dispatch_pipeline, - remap_1d_to_2d_dispatch_bind_group, - ) { - cull_pass.set_pipeline(remap_1d_to_2d_dispatch_pipeline); - cull_pass.set_push_constants(0, &max_compute_workgroups_per_dimension.to_be_bytes()); - cull_pass.set_bind_group(0, remap_1d_to_2d_dispatch_bind_group, &[]); - cull_pass.dispatch_workgroups(1, 1, 1); +fn remap_1d_to_2d( + mut pass: ComputePass, + pipeline: Option<&ComputePipeline>, + bind_group: Option<&BindGroup>, +) { + if let (Some(pipeline), Some(bind_group)) = (pipeline, bind_group) { + pass.set_pipeline(pipeline); + pass.set_bind_group(0, bind_group, &[]); + pass.dispatch_workgroups(1, 1, 1); } } fn raster_pass( first_pass: bool, render_context: &mut RenderContext, - visibility_buffer_hardware_software_indirect_args: &Buffer, + visibility_buffer_software_raster_indirect_args: &Buffer, visibility_buffer_hardware_raster_indirect_args: &Buffer, dummy_render_target: &TextureView, meshlet_view_bind_groups: &MeshletViewBindGroups, view_offset: &ViewUniformOffset, - visibility_buffer_hardware_software_pipeline: &ComputePipeline, + visibility_buffer_software_raster_pipeline: &ComputePipeline, visibility_buffer_hardware_raster_pipeline: &RenderPipeline, + fill_counts_pipeline: &ComputePipeline, camera: Option<&ExtractedCamera>, raster_cluster_rightmost_slot: u32, ) { - let command_encoder = render_context.command_encoder(); - let mut software_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { - label: Some(if first_pass { - "raster_software_first" - } else { - "raster_software_second" - }), - timestamp_writes: None, - }); - software_pass.set_pipeline(visibility_buffer_hardware_software_pipeline); + let mut software_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some(if first_pass { + "raster_software_first" + } else { + "raster_software_second" + }), + timestamp_writes: None, + }); + software_pass.set_pipeline(visibility_buffer_software_raster_pipeline); software_pass.set_bind_group( 0, &meshlet_view_bind_groups.visibility_buffer_raster, &[view_offset.offset], ); - software_pass - .dispatch_workgroups_indirect(visibility_buffer_hardware_software_indirect_args, 0); + software_pass.dispatch_workgroups_indirect(visibility_buffer_software_raster_indirect_args, 0); drop(software_pass); let mut hardware_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { @@ -508,6 +620,18 @@ fn raster_pass( &[view_offset.offset], ); hardware_pass.draw_indirect(visibility_buffer_hardware_raster_indirect_args, 0); + drop(hardware_pass); + + let mut fill_counts_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("fill_counts"), + timestamp_writes: None, + }); + fill_counts_pass.set_pipeline(fill_counts_pipeline); + fill_counts_pass.set_bind_group(0, &meshlet_view_bind_groups.fill_counts, &[]); + fill_counts_pass.dispatch_workgroups(1, 1, 1); } fn resolve_depth( diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl index 4c56c5874a..8d8a22b943 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -4,9 +4,8 @@ meshlet_bindings::{ Meshlet, meshlet_visibility_buffer, - meshlet_cluster_meshlet_ids, + meshlet_raster_clusters, meshlets, - meshlet_cluster_instance_ids, meshlet_instance_uniforms, get_meshlet_vertex_id, get_meshlet_vertex_position, @@ -106,7 +105,8 @@ struct VertexOutput { fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { let packed_ids = u32(textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy)).r); let cluster_id = packed_ids >> 7u; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + let meshlet_id = instanced_offset.offset; var meshlet = meshlets[meshlet_id]; let triangle_id = extractBits(packed_ids, 0u, 7u); @@ -116,7 +116,7 @@ fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { let vertex_1 = load_vertex(&meshlet, vertex_ids[1]); let vertex_2 = load_vertex(&meshlet, vertex_ids[2]); - let instance_id = meshlet_cluster_instance_ids[cluster_id]; + let instance_id = instanced_offset.instance_id; var instance_uniform = meshlet_instance_uniforms[instance_id]; let world_from_local = affine3_to_square(instance_uniform.world_from_local); diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl index 60f6f1b3ea..0ddfff8964 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl @@ -5,6 +5,7 @@ meshlet_cluster_instance_ids, meshlet_instance_uniforms, meshlet_raster_clusters, + meshlet_previous_raster_counts, meshlet_software_raster_cluster_count, meshlet_visibility_buffer, view, @@ -40,12 +41,11 @@ fn rasterize_cluster( if workgroup_id_1d >= meshlet_software_raster_cluster_count { return; } #endif - let cluster_id = meshlet_raster_clusters[workgroup_id_1d]; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; - var meshlet = meshlets[meshlet_id]; + let cluster_id = workgroup_id_1d + meshlet_previous_raster_counts[0]; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; - let instance_id = meshlet_cluster_instance_ids[cluster_id]; - let instance_uniform = meshlet_instance_uniforms[instance_id]; + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; let world_from_local = affine3_to_square(instance_uniform.world_from_local); // Load and project 1 vertex per thread, and then again if there are more than 128 vertices in the meshlet 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/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index cbd8445483..0207a81ed0 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1185,7 +1185,8 @@ impl AsBindGroupShaderType for StandardMaterial { bitflags! { /// The pipeline key for `StandardMaterial`, packed into 64 bits. - #[derive(Clone, Copy, PartialEq, Eq, Hash)] + #[repr(C)] + #[derive(Clone, Copy, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)] pub struct StandardMaterialKey: u64 { const CULL_FRONT = 0x000001; const CULL_BACK = 0x000002; @@ -1404,7 +1405,7 @@ impl Material for StandardMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 03a797eba1..0dda6127f0 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -2,17 +2,19 @@ mod prepass_bindings; use crate::{ alpha_mode_pipeline_key, binding_arrays_are_usable, buffer_layout, - collect_meshes_for_gpu_building, material_bind_groups::MaterialBindGroupAllocator, - queue_material_meshes, set_mesh_motion_vector_flags, setup_morph_and_skinning_defs, skin, - DrawMesh, EntitySpecializationTicks, Material, MaterialPipeline, MaterialPipelineKey, - MeshLayouts, MeshPipeline, MeshPipelineKey, OpaqueRendererMethod, PreparedMaterial, + collect_meshes_for_gpu_building, set_mesh_motion_vector_flags, setup_morph_and_skinning_defs, + skin, DeferredDrawFunction, DeferredFragmentShader, DeferredVertexShader, DrawMesh, + EntitySpecializationTicks, ErasedMaterialPipelineKey, Material, MaterialPipeline, + MaterialProperties, MeshLayouts, MeshPipeline, MeshPipelineKey, OpaqueRendererMethod, + PreparedMaterial, PrepassDrawFunction, PrepassFragmentShader, PrepassVertexShader, RenderLightmaps, RenderMaterialInstances, RenderMeshInstanceFlags, RenderMeshInstances, - RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, StandardMaterial, + RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, }; use bevy_app::{App, Plugin, PreUpdate}; use bevy_render::{ alpha::AlphaMode, batching::gpu_preprocessing::GpuPreprocessingSupport, + load_shader_library, mesh::{allocator::MeshAllocator, Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::prepare_assets, render_resource::binding_types::uniform_buffer, @@ -23,7 +25,7 @@ use bevy_render::{ }; pub use prepass_bindings::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetServer, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, deferred::*, prelude::Camera3d, prepass::*, }; @@ -54,70 +56,31 @@ use crate::meshlet::{ MeshletMesh3d, }; +use alloc::sync::Arc; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::component::Tick; -use bevy_ecs::system::SystemChangeTick; +use bevy_ecs::{component::Tick, system::SystemChangeTick}; use bevy_platform::collections::HashMap; -use bevy_render::sync_world::MainEntityHashMap; -use bevy_render::view::RenderVisibleEntities; -use bevy_render::RenderSystems::{PrepareAssets, PrepareResources}; -use core::{hash::Hash, marker::PhantomData}; - -pub const PREPASS_SHADER_HANDLE: Handle = - weak_handle!("ce810284-f1ae-4439-ab2e-0d6b204b6284"); - -pub const PREPASS_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("3e83537e-ae17-489c-a18a-999bc9c1d252"); - -pub const PREPASS_UTILS_SHADER_HANDLE: Handle = - weak_handle!("02e4643a-a14b-48eb-a339-0c47aeab0d7e"); - -pub const PREPASS_IO_SHADER_HANDLE: Handle = - weak_handle!("1c065187-c99b-4b7c-ba59-c1575482d2c9"); +use bevy_render::{ + erased_render_asset::ErasedRenderAssets, + sync_world::MainEntityHashMap, + view::RenderVisibleEntities, + RenderSystems::{PrepareAssets, PrepareResources}, +}; +use bevy_utils::default; +use core::marker::PhantomData; /// Sets up everything required to use the prepass pipeline. /// /// This does not add the actual prepasses, see [`PrepassPlugin`] for that. -pub struct PrepassPipelinePlugin(PhantomData); +pub struct PrepassPipelinePlugin; -impl Default for PrepassPipelinePlugin { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Plugin for PrepassPipelinePlugin -where - M::Data: PartialEq + Eq + Hash + Clone, -{ +impl Plugin for PrepassPipelinePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - PREPASS_SHADER_HANDLE, - "prepass.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "prepass.wgsl"); - load_internal_asset!( - app, - PREPASS_BINDINGS_SHADER_HANDLE, - "prepass_bindings.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - PREPASS_UTILS_SHADER_HANDLE, - "prepass_utils.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - PREPASS_IO_SHADER_HANDLE, - "prepass_io.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "prepass_bindings.wgsl"); + load_shader_library!(app, "prepass_utils.wgsl"); + load_shader_library!(app, "prepass_io.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -126,11 +89,9 @@ where render_app .add_systems( Render, - prepare_prepass_view_bind_group::.in_set(RenderSystems::PrepareBindGroups), + prepare_prepass_view_bind_group.in_set(RenderSystems::PrepareBindGroups), ) - .init_resource::() - .init_resource::>>() - .allow_ambiguous_resource::>>(); + .init_resource::>(); } fn finish(&self, app: &mut App) { @@ -138,33 +99,28 @@ where return; }; - render_app.init_resource::>(); + render_app + .init_resource::() + .init_resource::(); } } -/// Sets up the prepasses for a [`Material`]. +/// Sets up the prepasses for a material. /// /// This depends on the [`PrepassPipelinePlugin`]. -pub struct PrepassPlugin { +pub struct PrepassPlugin { /// Debugging flags that can optionally be set when constructing the renderer. pub debug_flags: RenderDebugFlags, - pub phantom: PhantomData, } -impl PrepassPlugin { +impl PrepassPlugin { /// Creates a new [`PrepassPlugin`] with the given debug flags. pub fn new(debug_flags: RenderDebugFlags) -> Self { - PrepassPlugin { - debug_flags, - phantom: PhantomData, - } + PrepassPlugin { debug_flags } } } -impl Plugin for PrepassPlugin -where - M::Data: PartialEq + Eq + Hash + Clone, -{ +impl Plugin for PrepassPlugin { fn build(&self, app: &mut App) { let no_prepass_plugin_loaded = app .world() @@ -206,41 +162,45 @@ where render_app .init_resource::() .init_resource::() - .init_resource::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() .add_systems( Render, ( check_prepass_views_need_specialization.in_set(PrepareAssets), - specialize_prepass_material_meshes:: + specialize_prepass_material_meshes .in_set(RenderSystems::PrepareMeshes) - .after(prepare_assets::>) .after(prepare_assets::) .after(collect_meshes_for_gpu_building) .after(set_mesh_motion_vector_flags), - queue_prepass_material_meshes:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - // queue_material_meshes only writes to `material_bind_group_id`, which `queue_prepass_material_meshes` doesn't read - .ambiguous_with(queue_material_meshes::), + queue_prepass_material_meshes.in_set(RenderSystems::QueueMeshes), ), ); #[cfg(feature = "meshlet")] render_app.add_systems( Render, - prepare_material_meshlet_meshes_prepass:: + prepare_material_meshlet_meshes_prepass .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - .before(queue_material_meshlet_meshes::) + .before(queue_material_meshlet_meshes) .run_if(resource_exists::), ); } } +/// Marker resource for whether prepass is enabled globally for this material type +#[derive(Resource, Debug)] +pub struct PrepassEnabled(PhantomData); + +impl Default for PrepassEnabled { + fn default() -> Self { + PrepassEnabled(PhantomData) + } +} + #[derive(Resource)] struct AnyPrepassPluginLoaded; @@ -249,11 +209,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, }); } } @@ -288,23 +253,13 @@ pub fn update_mesh_previous_global_transforms( } } -#[derive(Resource)] -pub struct PrepassPipeline { - pub internal: PrepassPipelineInternal, - pub material_pipeline: MaterialPipeline, -} - -/// Internal fields of the `PrepassPipeline` that don't need the generic bound -/// This is done as an optimization to not recompile the same code multiple time -pub struct PrepassPipelineInternal { +#[derive(Resource, Clone)] +pub struct PrepassPipeline { pub view_layout_motion_vectors: BindGroupLayout, pub view_layout_no_motion_vectors: BindGroupLayout, pub mesh_layouts: MeshLayouts, - pub material_layout: BindGroupLayout, - pub prepass_material_vertex_shader: Option>, - pub prepass_material_fragment_shader: Option>, - pub deferred_material_vertex_shader: Option>, - pub deferred_material_fragment_shader: Option>, + pub empty_layout: BindGroupLayout, + pub default_prepass_shader: Handle, /// Whether skins will use uniform buffers on account of storage buffers /// being unavailable on this platform. @@ -315,14 +270,13 @@ pub struct PrepassPipelineInternal { /// Whether binding arrays (a.k.a. bindless textures) are usable on the /// current render device. pub binding_arrays_are_usable: bool, + pub material_pipeline: MaterialPipeline, } -impl FromWorld for PrepassPipeline { +impl FromWorld for PrepassPipeline { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); let render_adapter = world.resource::(); - let asset_server = world.resource::(); - let visibility_ranges_buffer_binding_type = render_device .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); @@ -379,47 +333,27 @@ impl FromWorld for PrepassPipeline { let depth_clip_control_supported = render_device .features() .contains(WgpuFeatures::DEPTH_CLIP_CONTROL); - let internal = PrepassPipelineInternal { + PrepassPipeline { view_layout_motion_vectors, view_layout_no_motion_vectors, mesh_layouts: mesh_pipeline.mesh_layouts.clone(), - prepass_material_vertex_shader: match M::prepass_vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - prepass_material_fragment_shader: match M::prepass_fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - deferred_material_vertex_shader: match M::deferred_vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - deferred_material_fragment_shader: match M::deferred_fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - material_layout: M::bind_group_layout(render_device), + default_prepass_shader: load_embedded_asset!(world, "prepass.wgsl"), 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), - }; - PrepassPipeline { - internal, - material_pipeline: world.resource::>().clone(), + empty_layout: render_device.create_bind_group_layout("prepass_empty_layout", &[]), + material_pipeline: world.resource::().clone(), } } } -impl SpecializedMeshPipeline for PrepassPipeline -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - type Key = MaterialPipelineKey; +pub struct PrepassPipelineSpecializer { + pub(crate) pipeline: PrepassPipeline, + pub(crate) properties: Arc, +} + +impl SpecializedMeshPipeline for PrepassPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; fn specialize( &self, @@ -427,37 +361,46 @@ where layout: &MeshVertexBufferLayoutRef, ) -> Result { let mut shader_defs = Vec::new(); - if self.material_pipeline.bindless { + if self.properties.bindless { shader_defs.push("BINDLESS".into()); } - let mut descriptor = self - .internal - .specialize(key.mesh_key, shader_defs, layout)?; + let mut descriptor = + self.pipeline + .specialize(key.mesh_key, shader_defs, layout, &self.properties)?; // This is a bit risky because it's possible to change something that would // break the prepass but be fine in the main pass. // Since this api is pretty low-level it doesn't matter that much, but it is a potential issue. - M::specialize(&self.material_pipeline, &mut descriptor, layout, key)?; + if let Some(specialize) = self.properties.specialize { + specialize( + &self.pipeline.material_pipeline, + &mut descriptor, + layout, + key, + )?; + } Ok(descriptor) } } -impl PrepassPipelineInternal { +impl PrepassPipeline { fn specialize( &self, mesh_key: MeshPipelineKey, shader_defs: Vec, layout: &MeshVertexBufferLayoutRef, + material_properties: &MaterialProperties, ) -> 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. @@ -467,7 +410,13 @@ impl PrepassPipelineInternal { // NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material. // The main limitation right now is that bind group order is hardcoded in shaders. - bind_group_layouts.push(self.material_layout.clone()); + bind_group_layouts.push( + material_properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ); #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("WEBGL2".into()); shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); @@ -582,7 +531,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( @@ -603,59 +552,57 @@ impl PrepassPipelineInternal { let fragment_required = !targets.is_empty() || emulate_unclipped_depth || (mesh_key.contains(MeshPipelineKey::MAY_DISCARD) - && self.prepass_material_fragment_shader.is_some()); + && material_properties + .get_shader(PrepassFragmentShader) + .is_some()); let fragment = fragment_required.then(|| { // Use the fragment shader from the material let frag_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - match self.deferred_material_fragment_shader.clone() { + match material_properties.get_shader(DeferredFragmentShader) { Some(frag_shader_handle) => frag_shader_handle, - _ => PREPASS_SHADER_HANDLE, + None => self.default_prepass_shader.clone(), } } else { - match self.prepass_material_fragment_shader.clone() { + match material_properties.get_shader(PrepassFragmentShader) { Some(frag_shader_handle) => frag_shader_handle, - _ => PREPASS_SHADER_HANDLE, + None => self.default_prepass_shader.clone(), } }; FragmentState { shader: frag_shader_handle, - entry_point: "fragment".into(), shader_defs: shader_defs.clone(), targets, + ..default() } }); // Use the vertex shader from the material if present let vert_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - if let Some(handle) = &self.deferred_material_vertex_shader { - handle.clone() + if let Some(handle) = material_properties.get_shader(DeferredVertexShader) { + handle } else { - PREPASS_SHADER_HANDLE + self.default_prepass_shader.clone() } - } else if let Some(handle) = &self.prepass_material_vertex_shader { - handle.clone() + } else if let Some(handle) = material_properties.get_shader(PrepassVertexShader) { + handle } else { - PREPASS_SHADER_HANDLE + self.default_prepass_shader.clone() }; let descriptor = RenderPipelineDescriptor { vertex: VertexState { shader: vert_shader_handle, - entry_point: "vertex".into(), shader_defs, buffers: vec![vertex_buffer_layout], + ..default() }, fragment, layout: bind_group_layouts, primitive: PrimitiveState { topology: mesh_key.primitive_topology(), - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: None, unclipped_depth, - polygon_mode: PolygonMode::Fill, - conservative: false, + ..default() }, depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -678,9 +625,8 @@ impl PrepassPipelineInternal { mask: !0, alpha_to_coverage_enabled: false, }, - push_constant_ranges: vec![], label: Some("prepass_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() }; Ok(descriptor) } @@ -729,11 +675,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, } } }; @@ -744,15 +695,34 @@ 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, } -pub fn prepare_prepass_view_bind_group( +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.empty_layout, + &[], + ); + PrepassViewBindGroup { + motion_vectors: None, + no_motion_vectors: None, + empty_bind_group, + } + } +} + +pub fn prepare_prepass_view_bind_group( render_device: Res, - prepass_pipeline: Res>, + prepass_pipeline: Res, view_uniforms: Res, globals_buffer: Res, previous_view_uniforms: Res, @@ -766,7 +736,7 @@ pub fn prepare_prepass_view_bind_group( ) { prepass_view_bind_group.no_motion_vectors = Some(render_device.create_bind_group( "prepass_view_no_motion_vectors_bind_group", - &prepass_pipeline.internal.view_layout_no_motion_vectors, + &prepass_pipeline.view_layout_no_motion_vectors, &BindGroupEntries::with_indices(( (0, view_binding.clone()), (1, globals_binding.clone()), @@ -777,7 +747,7 @@ pub fn prepare_prepass_view_bind_group( if let Some(previous_view_uniforms_binding) = previous_view_uniforms.uniforms.binding() { prepass_view_bind_group.motion_vectors = Some(render_device.create_bind_group( "prepass_view_motion_vectors_bind_group", - &prepass_pipeline.internal.view_layout_motion_vectors, + &prepass_pipeline.view_layout_motion_vectors, &BindGroupEntries::with_indices(( (0, view_binding), (1, globals_binding), @@ -790,40 +760,20 @@ pub fn prepare_prepass_view_bind_group( } /// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view. -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedPrepassMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialPipelineCache { // view_entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } /// Stores the cached render pipeline ID for each entity in a single view, as /// well as the last time it was changed. -#[derive(Deref, DerefMut)] -pub struct SpecializedPrepassMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialViewPipelineCache { // material entity -> (tick, pipeline_id) #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedPrepassMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedPrepassMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } } #[derive(Resource, Deref, DerefMut, Default, Clone)] @@ -868,14 +818,13 @@ pub fn check_prepass_views_need_specialization( } } -pub fn specialize_prepass_material_meshes( +pub fn specialize_prepass_material_meshes( render_meshes: Res>, - render_materials: Res>>, + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, render_lightmaps: Res, render_visibility_ranges: Res, - material_bind_group_allocator: Res>, view_key_cache: Res, views: Query<( &ExtractedView, @@ -904,18 +853,15 @@ pub fn specialize_prepass_material_meshes( view_specialization_ticks, entity_specialization_ticks, ): ( - ResMut>, + ResMut, SystemChangeTick, - Res>, - ResMut>>, + Res, + ResMut>, Res, Res, - Res>, + Res, ), -) where - M: Material, - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views { if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) @@ -942,9 +888,6 @@ pub fn specialize_prepass_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; @@ -960,15 +903,14 @@ pub fn specialize_prepass_material_meshes( if !needs_specialization { continue; } - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - warn!("Couldn't get bind group for material"); + if !material.properties.prepass_enabled && !material.properties.shadows_enabled { + // If the material was previously specialized for prepass, remove it + view_specialized_material_pipeline_cache.remove(visible_entity); continue; - }; + } let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; @@ -1043,15 +985,19 @@ pub fn specialize_prepass_material_meshes( } } + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let prepass_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; let pipeline_id = pipelines.specialize( &pipeline_cache, - &prepass_pipeline, - MaterialPipelineKey { - mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, + &prepass_pipeline_specializer, + erased_key, &mesh.layout, ); let pipeline_id = match pipeline_id { @@ -1068,9 +1014,9 @@ pub fn specialize_prepass_material_meshes( } } -pub fn queue_prepass_material_meshes( +pub fn queue_prepass_material_meshes( render_mesh_instances: Res, - render_materials: Res>>, + render_materials: Res>, render_material_instances: Res, mesh_allocator: Res, gpu_preprocessing_support: Res, @@ -1079,10 +1025,8 @@ pub fn queue_prepass_material_meshes( mut opaque_deferred_render_phases: ResMut>, mut alpha_mask_deferred_render_phases: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities)>, - specialized_material_pipeline_cache: Res>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ + specialized_material_pipeline_cache: Res, +) { for (extracted_view, visible_entities) in &views { let ( mut opaque_phase, @@ -1135,14 +1079,11 @@ pub fn queue_prepass_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); @@ -1160,7 +1101,7 @@ pub fn queue_prepass_material_meshes( OpaqueNoLightmap3dBatchSetKey { draw_function: material .properties - .deferred_draw_function_id + .get_draw_function(DeferredDrawFunction) .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), @@ -1185,7 +1126,7 @@ pub fn queue_prepass_material_meshes( OpaqueNoLightmap3dBatchSetKey { draw_function: material .properties - .prepass_draw_function_id + .get_draw_function(PrepassDrawFunction) .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), @@ -1210,7 +1151,10 @@ pub fn queue_prepass_material_meshes( let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.deferred_draw_function_id.unwrap(), + draw_function: material + .properties + .get_draw_function(DeferredDrawFunction) + .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1234,7 +1178,10 @@ pub fn queue_prepass_material_meshes( let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.prepass_draw_function_id.unwrap(), + draw_function: material + .properties + .get_draw_function(PrepassDrawFunction) + .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1305,15 +1252,35 @@ impl RenderCommand

for SetPrepassViewBindGroup< ); } } - RenderCommandResult::Success } } -pub type DrawPrepass = ( +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 + } +} + +pub type DrawPrepass = ( SetItemPipeline, SetPrepassViewBindGroup<0>, - SetMeshBindGroup<1>, - SetMaterialBindGroup, + SetPrepassViewEmptyBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup<3>, 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/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 9d7fd0b18d..fa09120725 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -1,11 +1,11 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_ecs::prelude::*; use bevy_math::{Vec3, Vec4}; use bevy_render::{ extract_component::ExtractComponentPlugin, - render_resource::{DynamicUniformBuffer, Shader, ShaderType}, + load_shader_library, + render_resource::{DynamicUniformBuffer, ShaderType}, renderer::{RenderDevice, RenderQueue}, view::ExtractedView, Render, RenderApp, RenderSystems, @@ -126,15 +126,12 @@ pub struct ViewFogUniformOffset { pub offset: u32, } -/// Handle for the fog WGSL Shader internal asset -pub const FOG_SHADER_HANDLE: Handle = weak_handle!("e943f446-2856-471c-af5e-68dd276eec42"); - /// A plugin that consolidates fog extraction, preparation and related resources/assets pub struct FogPlugin; impl Plugin for FogPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, FOG_SHADER_HANDLE, "fog.wgsl", Shader::from_wgsl); + load_shader_library!(app, "fog.wgsl"); app.register_type::(); app.add_plugins(ExtractComponentPlugin::::default()); diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 5356c7580e..52df74cc26 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -9,7 +9,7 @@ use core::num::{NonZero, NonZeroU64}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, experimental::mip_generation::ViewDepthPyramid, @@ -38,7 +38,7 @@ use bevy_render::{ UntypedPhaseBatchedInstanceBuffers, }, experimental::occlusion_culling::OcclusionCulling, - render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext}, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt}, render_resource::{ binding_types::{storage_buffer, storage_buffer_read_only, texture_2d, uniform_buffer}, BindGroup, BindGroupEntries, BindGroupLayout, BindingResource, Buffer, BufferBinding, @@ -52,7 +52,7 @@ use bevy_render::{ view::{ExtractedView, NoIndirectDrawing, ViewUniform, ViewUniformOffset, ViewUniforms}, Render, RenderApp, RenderSystems, }; -use bevy_utils::TypeIdMap; +use bevy_utils::{default, TypeIdMap}; use bitflags::bitflags; use smallvec::{smallvec, SmallVec}; use tracing::warn; @@ -63,16 +63,6 @@ use crate::{ use super::{ShadowView, ViewLightEntities}; -/// The handle to the `mesh_preprocess.wgsl` compute shader. -pub const MESH_PREPROCESS_SHADER_HANDLE: Handle = - weak_handle!("c8579292-cf92-43b5-9c5a-ec5bd4e44d12"); -/// The handle to the `reset_indirect_batch_sets.wgsl` compute shader. -pub const RESET_INDIRECT_BATCH_SETS_SHADER_HANDLE: Handle = - weak_handle!("045fb176-58e2-4e76-b241-7688d761bb23"); -/// The handle to the `build_indirect_params.wgsl` compute shader. -pub const BUILD_INDIRECT_PARAMS_SHADER_HANDLE: Handle = - weak_handle!("133b01f0-3eaf-4590-9ee9-f0cf91a00b71"); - /// The GPU workgroup size. const WORKGROUP_SIZE: usize = 64; @@ -255,6 +245,8 @@ pub struct PreprocessPhasePipelines { pub struct PreprocessPipeline { /// The bind group layout for the compute shader. pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, /// The pipeline ID for the compute shader. /// /// This gets filled in `prepare_preprocess_pipelines`. @@ -269,6 +261,8 @@ pub struct PreprocessPipeline { pub struct ResetIndirectBatchSetsPipeline { /// The bind group layout for the compute shader. pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, /// The pipeline ID for the compute shader. /// /// This gets filled in `prepare_preprocess_pipelines`. @@ -280,6 +274,8 @@ pub struct ResetIndirectBatchSetsPipeline { pub struct BuildIndirectParametersPipeline { /// The bind group layout for the compute shader. pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, /// The pipeline ID for the compute shader. /// /// This gets filled in `prepare_preprocess_pipelines`. @@ -431,24 +427,9 @@ pub struct SkipGpuPreprocess; impl Plugin for GpuMeshPreprocessPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - MESH_PREPROCESS_SHADER_HANDLE, - "mesh_preprocess.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - RESET_INDIRECT_BATCH_SETS_SHADER_HANDLE, - "reset_indirect_batch_sets.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - BUILD_INDIRECT_PARAMS_SHADER_HANDLE, - "build_indirect_params.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "mesh_preprocess.wgsl"); + embedded_asset!(app, "reset_indirect_batch_sets.wgsl"); + embedded_asset!(app, "build_indirect_params.wgsl"); } fn finish(&self, app: &mut App) { @@ -1292,10 +1273,9 @@ impl SpecializedComputePipeline for PreprocessPipeline { } else { vec![] }, - shader: MESH_PREPROCESS_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -1363,18 +1343,27 @@ impl FromWorld for PreprocessPipelines { &build_non_indexed_indirect_params_bind_group_layout_entries, ); + let preprocess_shader = load_embedded_asset!(world, "mesh_preprocess.wgsl"); + let reset_indirect_batch_sets_shader = + load_embedded_asset!(world, "reset_indirect_batch_sets.wgsl"); + let build_indirect_params_shader = + load_embedded_asset!(world, "build_indirect_params.wgsl"); + let preprocess_phase_pipelines = PreprocessPhasePipelines { reset_indirect_batch_sets: ResetIndirectBatchSetsPipeline { bind_group_layout: reset_indirect_batch_sets_bind_group_layout.clone(), + shader: reset_indirect_batch_sets_shader, pipeline_id: None, }, gpu_occlusion_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), pipeline_id: None, }, gpu_occlusion_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), pipeline_id: None, }, }; @@ -1382,27 +1371,33 @@ impl FromWorld for PreprocessPipelines { PreprocessPipelines { direct_preprocess: PreprocessPipeline { bind_group_layout: direct_bind_group_layout, + shader: preprocess_shader.clone(), pipeline_id: None, }, gpu_frustum_culling_preprocess: PreprocessPipeline { bind_group_layout: gpu_frustum_culling_bind_group_layout, + shader: preprocess_shader.clone(), pipeline_id: None, }, early_gpu_occlusion_culling_preprocess: PreprocessPipeline { bind_group_layout: gpu_early_occlusion_culling_bind_group_layout, + shader: preprocess_shader.clone(), pipeline_id: None, }, late_gpu_occlusion_culling_preprocess: PreprocessPipeline { bind_group_layout: gpu_late_occlusion_culling_bind_group_layout, + shader: preprocess_shader, pipeline_id: None, }, gpu_frustum_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), pipeline_id: None, }, gpu_frustum_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader, pipeline_id: None, }, early_phase: preprocess_phase_pipelines.clone(), @@ -1641,11 +1636,8 @@ impl SpecializedComputePipeline for ResetIndirectBatchSetsPipeline { ComputePipelineDescriptor { label: Some("reset indirect batch sets".into()), layout: vec![self.bind_group_layout.clone()], - push_constant_ranges: vec![], - shader: RESET_INDIRECT_BATCH_SETS_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + shader: self.shader.clone(), + ..default() } } } @@ -1695,11 +1687,9 @@ impl SpecializedComputePipeline for BuildIndirectParametersPipeline { ComputePipelineDescriptor { label: Some(label.into()), layout: vec![self.bind_group_layout.clone()], - push_constant_ranges: vec![], - shader: BUILD_INDIRECT_PARAMS_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index dfc7f679f3..74dc0ff15d 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,7 +1,6 @@ -use self::assign::ClusterableObjectType; -use crate::material_bind_groups::MaterialBindGroupAllocator; use crate::*; use bevy_asset::UntypedAssetId; +pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES}; use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; use bevy_derive::{Deref, DerefMut}; @@ -12,9 +11,17 @@ use bevy_ecs::{ prelude::*, system::lifetimeless::Read, }; -use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_light::cascade::Cascade; +use bevy_light::cluster::assign::{calculate_cluster_factors, ClusterableObjectType}; +use bevy_light::cluster::GlobalVisibleClusterableObjects; +use bevy_light::{ + spot_light_clip_from_view, spot_light_world_from_view, DirectionalLightShadowMap, + NotShadowCaster, PointLightShadowMap, +}; +use bevy_math::{ops, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::hash::FixedHasher; +use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::experimental::occlusion_culling::{ OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, }; @@ -44,7 +51,8 @@ use bevy_render::{ }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_utils::default; -use core::{hash::Hash, marker::PhantomData, ops::Range}; +use core::{hash::Hash, ops::Range}; +use decal::clustered::RenderClusteredDecals; #[cfg(feature = "trace")] use tracing::info_span; use tracing::{error, warn}; @@ -121,7 +129,7 @@ pub struct GpuDirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, - skip: u32, + decal_index: u32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -221,7 +229,17 @@ pub fn extract_lights( point_light_shadow_map: Extract>, directional_light_shadow_map: Extract>, global_visible_clusterable: Extract>, - cubemap_visible_entities: Extract>>, + previous_point_lights: Query< + Entity, + ( + With, + With, + ), + >, + previous_spot_lights: Query< + Entity, + (With, With), + >, point_lights: Extract< Query<( Entity, @@ -278,14 +296,20 @@ pub fn extract_lights( commands.insert_resource(directional_light_shadow_map.clone()); } - // Clear previous visible entities for all cubemapped lights as they might not be in the + // Clear previous visible entities for all point/spot lights as they might not be in the // `global_visible_clusterable` list anymore. commands.try_insert_batch( - cubemap_visible_entities + previous_point_lights .iter() .map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default())) .collect::>(), ); + commands.try_insert_batch( + previous_spot_lights + .iter() + .map(|render_entity| (render_entity, RenderVisibleMeshEntities::default())) + .collect::>(), + ); // This is the point light shadow map texel size for one face of the cube as a distance of 1.0 // world unit from the light. @@ -533,7 +557,7 @@ 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()) { @@ -543,7 +567,7 @@ pub(crate) fn add_light_view_entities( /// 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()) { @@ -552,7 +576,7 @@ pub(crate) fn extracted_light_removed( } pub(crate) fn remove_light_view_entities( - trigger: Trigger, + trigger: On, query: Query<&LightViewEntities>, mut commands: Commands, ) { @@ -567,63 +591,6 @@ pub(crate) fn remove_light_view_entities( } } -pub(crate) struct CubeMapFace { - pub(crate) target: Vec3, - pub(crate) up: Vec3, -} - -// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation -// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy. -// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection -// -// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered -// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in -// left-handed y-up cubemap coordinates. -pub(crate) const CUBE_MAP_FACES: [CubeMapFace; 6] = [ - // +X - CubeMapFace { - target: Vec3::X, - up: Vec3::Y, - }, - // -X - CubeMapFace { - target: Vec3::NEG_X, - up: Vec3::Y, - }, - // +Y - CubeMapFace { - target: Vec3::Y, - up: Vec3::Z, - }, - // -Y - CubeMapFace { - target: Vec3::NEG_Y, - up: Vec3::NEG_Z, - }, - // +Z (with left-handed conventions, pointing forwards) - CubeMapFace { - target: Vec3::NEG_Z, - up: Vec3::Y, - }, - // -Z (with left-handed conventions, pointing backwards) - CubeMapFace { - target: Vec3::Z, - up: Vec3::Y, - }, -]; - -fn face_index_to_name(face_index: usize) -> &'static str { - match face_index { - 0 => "+x", - 1 => "-x", - 2 => "+y", - 3 => "-y", - 4 => "+z", - 5 => "-z", - _ => "invalid", - } -} - #[derive(Component)] pub struct ShadowView { pub depth_attachment: DepthAttachment, @@ -677,54 +644,6 @@ pub enum LightEntity { light_entity: Entity, }, } -pub fn calculate_cluster_factors( - near: f32, - far: f32, - z_slices: f32, - is_orthographic: bool, -) -> Vec2 { - if is_orthographic { - Vec2::new(-near, z_slices / (-far - -near)) - } else { - let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / ops::ln(far / near); - Vec2::new( - z_slices_of_ln_zfar_over_znear, - ops::ln(near) * z_slices_of_ln_zfar_over_znear, - ) - } -} - -// this method of constructing a basis from a vec3 is used by glam::Vec3::any_orthonormal_pair -// we will also construct it in the fragment shader and need our implementations to match, -// so we reproduce it here to avoid a mismatch if glam changes. we also switch the handedness -// could move this onto transform but it's pretty niche -pub(crate) fn spot_light_world_from_view(transform: &GlobalTransform) -> Mat4 { - // the matrix z_local (opposite of transform.forward()) - let fwd_dir = transform.back().extend(0.0); - - let sign = 1f32.copysign(fwd_dir.z); - let a = -1.0 / (fwd_dir.z + sign); - let b = fwd_dir.x * fwd_dir.y * a; - let up_dir = Vec4::new( - 1.0 + sign * fwd_dir.x * fwd_dir.x * a, - sign * b, - -sign * fwd_dir.x, - 0.0, - ); - let right_dir = Vec4::new(-b, -sign - fwd_dir.y * fwd_dir.y * a, fwd_dir.y, 0.0); - - Mat4::from_cols( - right_dir, - up_dir, - fwd_dir, - transform.translation().extend(1.0), - ) -} - -pub(crate) fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { - // spot light projection FOV is 2x the angle from spot light center to outer edge - Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z) -} pub fn prepare_lights( mut commands: Commands, @@ -762,7 +681,10 @@ pub fn prepare_lights( directional_lights: Query<(Entity, &MainEntity, &ExtractedDirectionalLight)>, mut light_view_entities: Query<&mut LightViewEntities>, sorted_cameras: Res, - gpu_preprocessing_support: Res, + (gpu_preprocessing_support, decals): ( + Res, + Option>, + ), ) { let views_iter = views.iter(); let views_count = views_iter.len(); @@ -880,7 +802,7 @@ pub fn prepare_lights( // - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. point_lights.sort_by_cached_key(|(entity, _, light, _)| { ( - ClusterableObjectType::from_point_or_spot_light(light).ordering(), + point_or_spot_light_to_clusterable(light).ordering(), *entity, ) }); @@ -982,8 +904,12 @@ pub fn prepare_lights( shadow_normal_bias: light.shadow_normal_bias, shadow_map_near_z: light.shadow_map_near_z, spot_light_tan_angle, - pad_a: 0.0, - pad_b: 0.0, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), + pad: 0.0, soft_shadow_size: if light.soft_shadows_enabled { light.radius } else { @@ -993,57 +919,37 @@ pub fn prepare_lights( global_light_meta.entity_to_index.insert(entity, index); } - let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; + // iterate the views once to find the maximum number of cascade shadowmaps we will need let mut num_directional_cascades_enabled = 0usize; - for (index, (_light_entity, _, light)) in directional_lights + for ( + _entity, + _camera_main_entity, + _extracted_view, + _clusters, + maybe_layers, + _no_indirect_drawing, + _maybe_ambient_override, + ) in sorted_cameras + .0 .iter() - .enumerate() - .take(MAX_DIRECTIONAL_LIGHTS) + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) { - let mut flags = DirectionalLightFlags::NONE; + let mut num_directional_cascades_for_this_view = 0usize; + let render_layers = maybe_layers.unwrap_or_default(); - // Lights are sorted, volumetric and shadow enabled lights are first - if light.volumetric - && light.shadows_enabled - && (index < directional_volumetric_enabled_count) - { - flags |= DirectionalLightFlags::VOLUMETRIC; - } - // Shadow enabled lights are second - if light.shadows_enabled && (index < directional_shadow_enabled_count) { - flags |= DirectionalLightFlags::SHADOWS_ENABLED; + for (_light_entity, _, light) in directional_lights.iter() { + if light.shadows_enabled && light.render_layers.intersects(render_layers) { + num_directional_cascades_for_this_view += light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + } } - if light.affects_lightmapped_mesh_diffuse { - flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; - } - - let num_cascades = light - .cascade_shadow_config - .bounds - .len() - .min(MAX_CASCADES_PER_LIGHT); - gpu_directional_lights[index] = GpuDirectionalLight { - // Set to true later when necessary. - skip: 0u32, - // Filled in later. - cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], - // premultiply color by illuminance - // we don't use the alpha at all, so no reason to multiply only [0..3] - color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, - // direction is negated to be ready for N.L - dir_to_light: light.transform.back().into(), - flags: flags.bits(), - soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), - shadow_depth_bias: light.shadow_depth_bias, - shadow_normal_bias: light.shadow_normal_bias, - num_cascades: num_cascades as u32, - cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, - depth_texture_base_index: num_directional_cascades_enabled as u32, - }; - if index < directional_shadow_enabled_count { - num_directional_cascades_enabled += num_cascades; - } + num_directional_cascades_enabled = num_directional_cascades_enabled + .max(num_directional_cascades_for_this_view) + .min(max_texture_array_layers); } global_light_meta @@ -1168,6 +1074,7 @@ pub fn prepare_lights( { live_views.insert(entity); + let view_layers = maybe_layers.unwrap_or_default(); let mut view_lights = Vec::new(); let mut view_occlusion_culling_lights = Vec::new(); @@ -1187,6 +1094,73 @@ pub fn prepare_lights( let n_clusters = clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z; let ambient_light = maybe_ambient_override.unwrap_or(&ambient_light); + + let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; + let mut num_directional_cascades_enabled_for_this_view = 0usize; + let mut num_directional_lights_for_this_view = 0usize; + for (index, (light_entity, _, light)) in directional_lights + .iter() + .filter(|(_light_entity, _, light)| light.render_layers.intersects(view_layers)) + .enumerate() + .take(MAX_DIRECTIONAL_LIGHTS) + { + num_directional_lights_for_this_view += 1; + + let mut flags = DirectionalLightFlags::NONE; + + // Lights are sorted, volumetric and shadow enabled lights are first + if light.volumetric + && light.shadows_enabled + && (index < directional_volumetric_enabled_count) + { + flags |= DirectionalLightFlags::VOLUMETRIC; + } + + // Shadow enabled lights are second + let mut num_cascades = 0; + if light.shadows_enabled { + let cascades = light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + + if num_directional_cascades_enabled_for_this_view + cascades + <= max_texture_array_layers + { + flags |= DirectionalLightFlags::SHADOWS_ENABLED; + num_cascades += cascades; + } + } + + if light.affects_lightmapped_mesh_diffuse { + flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; + } + + gpu_directional_lights[index] = GpuDirectionalLight { + // Filled in later. + cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], + // premultiply color by illuminance + // we don't use the alpha at all, so no reason to multiply only [0..3] + color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, + // direction is negated to be ready for N.L + dir_to_light: light.transform.back().into(), + flags: flags.bits(), + soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), + shadow_depth_bias: light.shadow_depth_bias, + shadow_normal_bias: light.shadow_normal_bias, + num_cascades: num_cascades as u32, + cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, + depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(*light_entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), + }; + num_directional_cascades_enabled_for_this_view += num_cascades; + } + let mut gpu_lights = GpuLights { directional_lights: gpu_directional_lights, ambient_color: Vec4::from_slice(&LinearRgba::from(ambient_light.color).to_f32_array()) @@ -1198,8 +1172,7 @@ pub fn prepare_lights( cluster_factors_zw.y, ), cluster_dimensions: clusters.dimensions.extend(n_clusters), - n_directional_lights: directional_lights.iter().len().min(MAX_DIRECTIONAL_LIGHTS) - as u32, + n_directional_lights: num_directional_lights_for_this_view as u32, // spotlight shadow maps are stored in the directional light array, starting at num_directional_cascades_enabled. // the spot lights themselves start in the light array at point_light_count. so to go from light // index to shadow map index, we need to subtract point light count and add directional shadowmap count. @@ -1429,27 +1402,31 @@ pub fn prepare_lights( } // directional lights + // clear entities for lights that don't intersect the layer + for &(light_entity, _, _) in directional_lights + .iter() + .filter(|(_, _, light)| !light.render_layers.intersects(view_layers)) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + } + let mut directional_depth_texture_array_index = 0u32; - let view_layers = maybe_layers.unwrap_or_default(); for (light_index, &(light_entity, light_main_entity, light)) in directional_lights .iter() + .filter(|(_, _, light)| light.render_layers.intersects(view_layers)) .enumerate() .take(MAX_DIRECTIONAL_LIGHTS) { - let gpu_light = &mut gpu_lights.directional_lights[light_index]; - let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { continue; }; - // Check if the light intersects with the view. - if !view_layers.intersects(&light.render_layers) { - gpu_light.skip = 1u32; - if let Some(entities) = light_view_entities.remove(&entity) { - despawn_entities(&mut commands, entities); - } - continue; - } + let gpu_light = &mut gpu_lights.directional_lights[light_index]; // Only deal with cascades when shadows are enabled. if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 { @@ -1656,37 +1633,17 @@ pub struct LightKeyCache(HashMap); #[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] pub struct LightSpecializationTicks(HashMap); -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedShadowMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialPipelineCache { // view light entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } -#[derive(Deref, DerefMut)] -pub struct SpecializedShadowMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialViewPipelineCache { #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedShadowMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedShadowMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: MainEntityHashMap::default(), - marker: PhantomData, - } - } } pub fn check_views_lights_need_specialization( @@ -1728,23 +1685,16 @@ pub fn check_views_lights_need_specialization( } } -pub fn specialize_shadows( - prepass_pipeline: Res>, - ( - render_meshes, - render_mesh_instances, - render_materials, - render_material_instances, - material_bind_group_allocator, - ): ( +pub fn specialize_shadows( + prepass_pipeline: Res, + (render_meshes, render_mesh_instances, render_materials, render_material_instances): ( Res>, Res, - Res>>, + Res>, Res, - Res>, ), shadow_render_phases: Res>, - mut pipelines: ResMut>>, + mut pipelines: ResMut>, pipeline_cache: Res, render_lightmaps: Res, view_lights: Query<(Entity, &ViewLightEntities), With>, @@ -1756,13 +1706,11 @@ pub fn specialize_shadows( >, spot_light_entities: Query<&RenderVisibleMeshEntities, With>, light_key_cache: Res, - mut specialized_material_pipeline_cache: ResMut>, + mut specialized_material_pipeline_cache: ResMut, light_specialization_ticks: Res, - entity_specialization_ticks: Res>, + entity_specialization_ticks: Res, ticks: SystemChangeTick, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { // Record the retained IDs of all shadow views so that we can expire old // pipeline IDs. let mut all_shadow_views: HashSet = HashSet::default(); @@ -1820,14 +1768,12 @@ pub fn specialize_shadows( .or_default(); for (_, visible_entity) in visible_entities.iter().copied() { - let Some(material_instances) = + let Some(material_instance) = render_material_instances.instances.get(&visible_entity) else { continue; }; - let Ok(material_asset_id) = material_instances.asset_id.try_typed::() else { - continue; - }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity) else { @@ -1844,20 +1790,19 @@ pub fn specialize_shadows( if !needs_specialization { continue; } - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; + if !material.properties.shadows_enabled { + // If the material is not a shadow caster, we don't need to specialize it. + continue; + } if !mesh_instance .flags .contains(RenderMeshInstanceFlags::SHADOW_CASTER) { continue; } - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - continue; - }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; @@ -1885,18 +1830,21 @@ pub fn specialize_shadows( | AlphaMode::AlphaToCoverage => MeshPipelineKey::MAY_DISCARD, _ => MeshPipelineKey::NONE, }; + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; let pipeline_id = pipelines.specialize( &pipeline_cache, - &prepass_pipeline, - MaterialPipelineKey { - mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, + &material_pipeline_specializer, + erased_key, &mesh.layout, ); - let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { @@ -1918,10 +1866,9 @@ pub fn specialize_shadows( /// For each shadow cascade, iterates over all the meshes "visible" from it and /// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as /// appropriate. -pub fn queue_shadows( - shadow_draw_functions: Res>, +pub fn queue_shadows( render_mesh_instances: Res, - render_materials: Res>>, + render_materials: Res>, render_material_instances: Res, mut shadow_render_phases: ResMut>, gpu_preprocessing_support: Res, @@ -1934,11 +1881,8 @@ pub fn queue_shadows( With, >, spot_light_entities: Query<&RenderVisibleMeshEntities, With>, - specialized_material_pipeline_cache: Res>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ - let draw_shadow_mesh = shadow_draw_functions.read().id::>(); + specialized_material_pipeline_cache: Res, +) { for (entity, view_lights) in &view_lights { for view_light_entity in view_lights.lights.iter().copied() { let Ok((light_entity, extracted_view_light)) = @@ -2009,10 +1953,12 @@ pub fn queue_shadows( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(draw_function) = + material.properties.get_draw_function(ShadowsDrawFunction) + else { continue; }; @@ -2021,7 +1967,7 @@ pub fn queue_shadows( let batch_set_key = ShadowBatchSetKey { pipeline: *pipeline_id, - draw_function: draw_shadow_mesh, + draw_function, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, @@ -2323,3 +2269,18 @@ impl ShadowPassNode { Ok(()) } } + +/// Creates the [`ClusterableObjectType`] data for a point or spot light. +fn point_or_spot_light_to_clusterable(point_light: &ExtractedPointLight) -> ClusterableObjectType { + match point_light.spot_light_angles { + Some((_, outer_angle)) => ClusterableObjectType::SpotLight { + outer_angle, + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + None => ClusterableObjectType::PointLight { + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + } +} diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 4fc6252cab..2e9562c5f8 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,6 +1,6 @@ use crate::material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot}; use allocator::MeshAllocator; -use bevy_asset::{load_internal_asset, weak_handle, AssetId}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetId}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, @@ -15,6 +15,9 @@ use bevy_ecs::{ system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_image::{BevyDefault, ImageSampler, TextureFormatPixelInfo}; +use bevy_light::{ + EnvironmentMapLight, NotShadowCaster, NotShadowReceiver, TransmittedShadowReceiver, +}; use bevy_math::{Affine3, Rect, UVec2, Vec3, Vec4}; use bevy_platform::collections::{hash_map::Entry, HashMap}; use bevy_render::{ @@ -52,7 +55,6 @@ use material_bind_groups::MaterialBindingId; use tracing::{error, warn}; use self::irradiance_volume::IRRADIANCE_VOLUMES_ARE_USABLE; -use crate::environment_map::EnvironmentMapLight; use crate::irradiance_volume::IrradianceVolume; use crate::{ render::{ @@ -101,22 +103,6 @@ impl MeshRenderPlugin { } } -pub const FORWARD_IO_HANDLE: Handle = weak_handle!("38111de1-6e35-4dbb-877b-7b6f9334baf6"); -pub const MESH_VIEW_TYPES_HANDLE: Handle = - weak_handle!("979493db-4ae1-4003-b5c6-fcbb88b152a2"); -pub const MESH_VIEW_BINDINGS_HANDLE: Handle = - weak_handle!("c6fe674b-4c21-4d4b-867a-352848da5337"); -pub const MESH_TYPES_HANDLE: Handle = weak_handle!("a4a3fc2e-a57e-4083-a8ab-2840176927f2"); -pub const MESH_BINDINGS_HANDLE: Handle = - weak_handle!("84e7f9e6-e566-4a61-914e-c568f5dabf49"); -pub const MESH_FUNCTIONS_HANDLE: Handle = - weak_handle!("c46aa0f0-6c0c-4b3a-80bf-d8213c771f12"); -pub const MESH_SHADER_HANDLE: Handle = weak_handle!("1a7bbae8-4b4f-48a7-b53b-e6822e56f321"); -pub const SKINNING_HANDLE: Handle = weak_handle!("7474e812-2506-4cbf-9de3-fe07e5c6ff24"); -pub const MORPH_HANDLE: Handle = weak_handle!("da30aac7-34cc-431d-a07f-15b1a783008c"); -pub const OCCLUSION_CULLING_HANDLE: Handle = - weak_handle!("eaea07d9-7516-482c-aa42-6f8e9927e1f0"); - /// How many textures are allowed in the view bind group layout (`@group(0)`) before /// broader compatibility with WebGL and WebGPU is at risk, due to the minimum guaranteed /// values for `MAX_TEXTURE_IMAGE_UNITS` (in WebGL) and `maxSampledTexturesPerShaderStage` (in WebGPU), @@ -130,45 +116,28 @@ pub const MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES: usize = 10; impl Plugin for MeshRenderPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, FORWARD_IO_HANDLE, "forward_io.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - MESH_VIEW_TYPES_HANDLE, - "mesh_view_types.wgsl", - Shader::from_wgsl_with_defs, - vec![ - ShaderDefVal::UInt( - "MAX_DIRECTIONAL_LIGHTS".into(), - MAX_DIRECTIONAL_LIGHTS as u32 - ), - ShaderDefVal::UInt( - "MAX_CASCADES_PER_LIGHT".into(), - MAX_CASCADES_PER_LIGHT as u32, - ) - ] - ); - load_internal_asset!( - app, - MESH_VIEW_BINDINGS_HANDLE, - "mesh_view_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, MESH_TYPES_HANDLE, "mesh_types.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - MESH_FUNCTIONS_HANDLE, - "mesh_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, MESH_SHADER_HANDLE, "mesh.wgsl", Shader::from_wgsl); - load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl); - load_internal_asset!(app, MORPH_HANDLE, "morph.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - OCCLUSION_CULLING_HANDLE, - "occlusion_culling.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "forward_io.wgsl"); + load_shader_library!(app, "mesh_view_types.wgsl", |settings| *settings = + ShaderSettings { + shader_defs: vec![ + ShaderDefVal::UInt( + "MAX_DIRECTIONAL_LIGHTS".into(), + MAX_DIRECTIONAL_LIGHTS as u32 + ), + ShaderDefVal::UInt( + "MAX_CASCADES_PER_LIGHT".into(), + MAX_CASCADES_PER_LIGHT as u32, + ) + ] + }); + load_shader_library!(app, "mesh_view_bindings.wgsl"); + load_shader_library!(app, "mesh_types.wgsl"); + load_shader_library!(app, "mesh_functions.wgsl"); + load_shader_library!(app, "skinning.wgsl"); + load_shader_library!(app, "morph.wgsl"); + load_shader_library!(app, "occlusion_culling.wgsl"); + + embedded_asset!(app, "mesh.wgsl"); if app.get_sub_app(RenderApp).is_none() { return; @@ -310,13 +279,10 @@ impl Plugin for MeshRenderPlugin { // Load the mesh_bindings shader module here as it depends on runtime information about // whether storage buffers are supported, or the maximum uniform buffer binding size. - load_internal_asset!( - app, - MESH_BINDINGS_HANDLE, - "mesh_bindings.wgsl", - Shader::from_wgsl_with_defs, - mesh_bindings_shader_defs - ); + load_shader_library!(app, "mesh_bindings.wgsl", move |settings| *settings = + ShaderSettings { + shader_defs: mesh_bindings_shader_defs.clone(), + }); } } @@ -1576,7 +1542,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, @@ -1787,6 +1753,8 @@ pub struct MeshPipeline { pub dummy_white_gpu_image: GpuImage, pub clustered_forward_buffer_binding_type: BufferBindingType, pub mesh_layouts: MeshLayouts, + /// The shader asset handle. + pub shader: Handle, /// `MeshUniform`s are stored in arrays in buffers. If storage buffers are available, they /// are used and this will be `None`, otherwise uniform buffers will be used with batches /// of this many `MeshUniform`s, stored at dynamic offsets within the uniform buffer. @@ -1816,6 +1784,7 @@ pub struct MeshPipeline { impl FromWorld for MeshPipeline { fn from_world(world: &mut World) -> Self { + let shader = load_embedded_asset!(world, "mesh.wgsl"); let mut system_state: SystemState<( Res, Res, @@ -1868,6 +1837,7 @@ impl FromWorld for MeshPipeline { clustered_forward_buffer_binding_type, dummy_white_gpu_image, mesh_layouts: MeshLayouts::new(&render_device, &render_adapter), + shader, per_object_buffer_batch_size: GpuArrayBuffer::::batch_size(&render_device), binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), clustered_decals_are_usable: decal::clustered::clustered_decals_are_usable( @@ -1896,7 +1866,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) } } @@ -2068,7 +2041,7 @@ impl GetFullBatchData for MeshPipeline { } bitflags::bitflags! { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] // NOTE: Apparently quadro drivers support up to 64x MSAA. /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. @@ -2352,7 +2325,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()); @@ -2582,6 +2559,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if self.clustered_decals_are_usable { shader_defs.push("CLUSTERED_DECALS_ARE_USABLE".into()); + if cfg!(feature = "pbr_light_textures") { + shader_defs.push("LIGHT_TEXTURES".into()); + } } let format = if key.contains(MeshPipelineKey::HDR) { @@ -2602,31 +2582,27 @@ impl SpecializedMeshPipeline for MeshPipeline { Ok(RenderPipelineDescriptor { vertex: VertexState { - shader: MESH_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.shader.clone(), shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { - shader: MESH_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend, write_mask: ColorWrites::ALL, })], + ..default() }), layout: bind_group_layout, - push_constant_ranges: vec![], primitive: PrimitiveState { - front_face: FrontFace::Ccw, cull_mode: Some(Face::Back), unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, topology: key.primitive_topology(), - strip_index_format: None, + ..default() }, depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -2650,7 +2626,7 @@ impl SpecializedMeshPipeline for MeshPipeline { alpha_to_coverage_enabled, }, label: Some(label), - zero_initialize_workgroup_memory: false, + ..default() }) } } @@ -2906,7 +2882,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>, @@ -2922,7 +2898,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_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 502b91b427..4c85192ddd 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -39,12 +39,9 @@ struct MorphWeights { #endif // [2^0, 2^16) -const MESH_FLAGS_VISIBILITY_RANGE_INDEX_BITS: u32 = 65535u; -// 2^28 -const MESH_FLAGS_NO_FRUSTUM_CULLING_BIT: u32 = 268435456u; -// 2^29 -const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 536870912u; -// 2^30 -const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 1073741824u; -// 2^31 - if the flag is set, the sign is positive, else it is negative -const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 2147483648u; +const MESH_FLAGS_VISIBILITY_RANGE_INDEX_BITS: u32 = (1u << 16u) - 1u; +const MESH_FLAGS_NO_FRUSTUM_CULLING_BIT: u32 = 1u << 28u; +const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u << 29u; +const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 1u << 30u; +// if the flag is set, the sign is positive, else it is negative +const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 1u << 31u; diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 8e231886ba..0f40327dc7 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -17,6 +17,7 @@ use bevy_ecs::{ world::{FromWorld, World}, }; use bevy_image::BevyDefault as _; +use bevy_light::EnvironmentMapLight; use bevy_math::Vec4; use bevy_render::{ globals::{GlobalsBuffer, GlobalsUniform}, @@ -30,7 +31,6 @@ use bevy_render::{ }, }; use core::{array, num::NonZero}; -use environment_map::EnvironmentMapLight; use crate::{ decal::{ @@ -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/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 6db72759df..3ba62f1414 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -13,14 +13,14 @@ struct ClusterableObject { spot_light_tan_angle: f32, soft_shadow_size: f32, shadow_map_near_z: f32, - texture_index: u32, + decal_index: u32, pad: f32, }; -const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; -const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u; -const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 4u; -const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 8u; +const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; +const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 1u << 1u; +const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 2u; +const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 3u; struct DirectionalCascade { clip_from_world: mat4x4, @@ -40,12 +40,12 @@ struct DirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, - skip: u32, + decal_index: u32, }; -const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; -const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 2u; -const DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 4u; +const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; +const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 1u; +const DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 2u; struct Lights { // NOTE: this array size must be kept in sync with the constants defined in bevy_pbr/src/render/light.rs 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/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index dcda30ee79..84f7b95661 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -422,7 +422,7 @@ fn apply_pbr_lighting( shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); } - let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse); + let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse, true); direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -442,7 +442,7 @@ fn apply_pbr_lighting( } let transmitted_light_contrib = - lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse); + lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse, true); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -511,9 +511,6 @@ fn apply_pbr_lighting( // check if this light should be skipped, which occurs if this light does not intersect with the view // note point and spot lights aren't skippable, as the relevant lights are filtered in `assign_lights_to_clusters` let light = &view_bindings::lights.directional_lights[i]; - if (*light).skip != 0u { - continue; - } // If we're lightmapped, disable diffuse contribution from the light if // requested, to avoid double-counting light. diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 01e09fe3b4..17cae13b92 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -456,10 +456,77 @@ fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { return clampedPerceptualRoughness * clampedPerceptualRoughness; } +// this must align with CubemapLayout in decal/clustered.rs +const CUBEMAP_TYPE_CROSS_VERTICAL: u32 = 0; +const CUBEMAP_TYPE_CROSS_HORIZONTAL: u32 = 1; +const CUBEMAP_TYPE_SEQUENCE_VERTICAL: u32 = 2; +const CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: u32 = 3; + +const X_PLUS: u32 = 0; +const X_MINUS: u32 = 1; +const Y_PLUS: u32 = 2; +const Y_MINUS: u32 = 3; +const Z_MINUS: u32 = 4; +const Z_PLUS: u32 = 5; + +fn cubemap_uv(direction: vec3, cubemap_type: u32) -> vec2 { + let abs_direction = abs(direction); + let max_axis = max(abs_direction.x, max(abs_direction.y, abs_direction.z)); + + let face_index = select( + select(X_PLUS, X_MINUS, direction.x < 0.0), + select( + select(Y_PLUS, Y_MINUS, direction.y < 0.0), + select(Z_PLUS, Z_MINUS, direction.z < 0.0), + max_axis != abs_direction.y + ), + max_axis != abs_direction.x + ); + + var face_uv: vec2; + var divisor: f32; + var corner_uv: vec2 = vec2(0, 0); + var face_size: vec2; + + switch face_index { + case X_PLUS: { face_uv = vec2(direction.z, -direction.y); divisor = direction.x; } + case X_MINUS: { face_uv = vec2(-direction.z, -direction.y); divisor = -direction.x; } + case Y_PLUS: { face_uv = vec2(direction.x, -direction.z); divisor = direction.y; } + case Y_MINUS: { face_uv = vec2(direction.x, direction.z); divisor = -direction.y; } + case Z_PLUS: { face_uv = vec2(direction.x, direction.y); divisor = direction.z; } + case Z_MINUS: { face_uv = vec2(direction.x, -direction.y); divisor = -direction.z; } + default: {} + } + face_uv = (face_uv / divisor) * 0.5 + 0.5; + + switch cubemap_type { + case CUBEMAP_TYPE_CROSS_VERTICAL: { + face_size = vec2(1.0/3.0, 1.0/4.0); + corner_uv = vec2((0x111102u >> (4 * face_index)) & 0xFu, (0x132011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_CROSS_HORIZONTAL: { + face_size = vec2(1.0/4.0, 1.0/3.0); + corner_uv = vec2((0x131102u >> (4 * face_index)) & 0xFu, (0x112011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: { + face_size = vec2(1.0/6.0, 1.0); + corner_uv.x = face_index; + } + case CUBEMAP_TYPE_SEQUENCE_VERTICAL: { + face_size = vec2(1.0, 1.0/6.0); + corner_uv.y = face_index; + } + default: {} + } + + return (vec2(corner_uv) + face_uv) * face_size; +} + fn point_light( light_id: u32, input: ptr, - enable_diffuse: bool + enable_diffuse: bool, + enable_texture: bool, ) -> vec3 { // Unpack. let diffuse_color = (*input).diffuse_color; @@ -555,8 +622,26 @@ fn point_light( color = diffuse + specular_light; #endif // STANDARD_MATERIAL_CLEARCOAT + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if enable_texture && (*light).decal_index != 0xFFFFFFFFu { + let relative_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * vec4(P, 1.0)).xyz; + let cubemap_type = view_bindings::clustered_decals.decals[(*light).decal_index].tag; + let decal_uv = cubemap_uv(relative_position, cubemap_type); + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } +#endif + return color * (*light).color_inverse_square_range.rgb * - (rangeAttenuation * derived_input.NdotL); + (rangeAttenuation * derived_input.NdotL) * texture_sample; } fn spot_light( @@ -565,7 +650,7 @@ fn spot_light( enable_diffuse: bool ) -> vec3 { // reuse the point light calculations - let point_light = point_light(light_id, input, enable_diffuse); + let point_light = point_light(light_id, input, enable_diffuse, false); let light = &view_bindings::clusterable_objects.data[light_id]; @@ -584,7 +669,27 @@ fn spot_light( let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w); let spot_attenuation = attenuation * attenuation; - return point_light * spot_attenuation; + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + if local_position.z < 0.0 { + let decal_uv = (local_position.xy / (local_position.z * (*light).spot_light_tan_angle)) * vec2(-0.5, 0.5) + 0.5; + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } + } +#endif + + return point_light * spot_attenuation * texture_sample; } fn directional_light( @@ -641,5 +746,31 @@ fn directional_light( color = (diffuse + specular_light) * derived_input.NdotL; #endif // STANDARD_MATERIAL_CLEARCOAT - return color * (*light).color.rgb; + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + let decal_uv = local_position.xy * vec2(-0.5, 0.5) + 0.5; + + // if tiled or within tile + if (view_bindings::clustered_decals.decals[(*light).decal_index].tag != 0u) + || all(clamp(decal_uv, vec2(0.0), vec2(1.0)) == decal_uv) + { + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv - floor(decal_uv), + 0.0 + ).r; + } else { + texture_sample = 0f; + } + } +#endif + + return color * (*light).color.rgb * texture_sample; } diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 29d479c4e3..b8b51c577e 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -34,36 +34,34 @@ struct StandardMaterial { // NOTE: if these flags are updated or changed. Be sure to also update // deferred_flags_from_mesh_material_flags and mesh_material_flags_from_deferred_flags // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -const STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u; -const STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 2u; -const STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u; -const STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u; -const STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u; -const STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u; -const STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 64u; -const STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 128u; -const STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT: u32 = 256u; -const STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT: u32 = 512u; -const STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT: u32 = 1024u; -const STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT: u32 = 2048u; -const STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT: u32 = 4096u; -const STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT: u32 = 8192u; -const STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT: u32 = 16384u; -const STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT: u32 = 32768u; -const STANDARD_MATERIAL_FLAGS_CLEARCOAT_NORMAL_TEXTURE_BIT: u32 = 65536u; -const STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT: u32 = 131072u; -const STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT: u32 = 262144u; -const STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT: u32 = 524288u; -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29) -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29) -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29) -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 1073741824u; // (2u32 << 29) -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED: u32 = 1610612736u; // (3u32 << 29) -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD: u32 = 2147483648u; // (4u32 << 29) -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 2684354560u; // (5u32 << 29) -const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE: u32 = 3221225472u; // (6u32 << 29) -// ↑ To calculate/verify the values above, use the following playground: -// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7792f8dd6fc6a8d4d0b6b1776898a7f4 +const STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u << 0u; +const STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 1u << 1u; +const STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 1u << 2u; +const STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 1u << 3u; +const STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 1u << 4u; +const STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 1u << 5u; +const STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 1u << 6u; +const STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 1u << 7u; +const STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT: u32 = 1u << 8u; +const STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT: u32 = 1u << 9u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT: u32 = 1u << 10u; +const STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT: u32 = 1u << 11u; +const STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT: u32 = 1u << 12u; +const STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT: u32 = 1u << 13u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT: u32 = 1u << 14u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT: u32 = 1u << 15u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_NORMAL_TEXTURE_BIT: u32 = 1u << 16u; +const STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT: u32 = 1u << 17u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT: u32 = 1u << 18u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT: u32 = 1u << 19u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 7u << 29u; // (0b111u << 29u) +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED: u32 = 3u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD: u32 = 4u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 5u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE: u32 = 6u << 29u; // Creates a StandardMaterial with default values diff --git a/crates/bevy_pbr/src/render/skin.rs b/crates/bevy_pbr/src/render/skin.rs index 476e06c1e7..f9ec672a66 100644 --- a/crates/bevy_pbr/src/render/skin.rs +++ b/crates/bevy_pbr/src/render/skin.rs @@ -220,7 +220,7 @@ pub fn prepare_skins( let mut new_size = uniform.current_buffer.size(); while new_size < needed_size { // 1.5× growth factor. - new_size += new_size / 2; + new_size = (new_size + new_size / 2).next_multiple_of(4); } // Create the new buffers. 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/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 9224374c60..001aa67c12 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -1,6 +1,6 @@ use crate::NodePbr; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, prelude::Camera3d, @@ -15,13 +15,15 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut}, world::{FromWorld, World}, }; +use bevy_image::ToExtents; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::{ExtractedCamera, TemporalJitter}, extract_component::ExtractComponent, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, prelude::Camera, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{ binding_types::{ sampler, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, @@ -39,38 +41,16 @@ use bevy_utils::prelude::default; use core::mem; use tracing::{error, warn}; -const PREPROCESS_DEPTH_SHADER_HANDLE: Handle = - weak_handle!("b7f2cc3d-c935-4f5c-9ae2-43d6b0d5659a"); -const SSAO_SHADER_HANDLE: Handle = weak_handle!("9ea355d7-37a2-4cc4-b4d1-5d8ab47b07f5"); -const SPATIAL_DENOISE_SHADER_HANDLE: Handle = - weak_handle!("0f2764a0-b343-471b-b7ce-ef5d636f4fc3"); -const SSAO_UTILS_SHADER_HANDLE: Handle = - weak_handle!("da53c78d-f318-473e-bdff-b388bc50ada2"); - /// Plugin for screen space ambient occlusion. pub struct ScreenSpaceAmbientOcclusionPlugin; impl Plugin for ScreenSpaceAmbientOcclusionPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - PREPROCESS_DEPTH_SHADER_HANDLE, - "preprocess_depth.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, SSAO_SHADER_HANDLE, "ssao.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - SPATIAL_DENOISE_SHADER_HANDLE, - "spatial_denoise.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SSAO_UTILS_SHADER_HANDLE, - "ssao_utils.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "ssao_utils.wgsl"); + + embedded_asset!(app, "preprocess_depth.wgsl"); + embedded_asset!(app, "ssao.wgsl"); + embedded_asset!(app, "spatial_denoise.wgsl"); app.register_type::(); @@ -321,6 +301,8 @@ struct SsaoPipelines { hilbert_index_lut: TextureView, point_clamp_sampler: Sampler, linear_clamp_sampler: Sampler, + + shader: Handle, } impl FromWorld for SsaoPipelines { @@ -430,11 +412,8 @@ impl FromWorld for SsaoPipelines { preprocess_depth_bind_group_layout.clone(), common_bind_group_layout.clone(), ], - push_constant_ranges: vec![], - shader: PREPROCESS_DEPTH_SHADER_HANDLE, - shader_defs: Vec::new(), - entry_point: "preprocess_depth".into(), - zero_initialize_workgroup_memory: false, + shader: load_embedded_asset!(world, "preprocess_depth.wgsl"), + ..default() }); let spatial_denoise_pipeline = @@ -444,11 +423,8 @@ impl FromWorld for SsaoPipelines { spatial_denoise_bind_group_layout.clone(), common_bind_group_layout.clone(), ], - push_constant_ranges: vec![], - shader: SPATIAL_DENOISE_SHADER_HANDLE, - shader_defs: Vec::new(), - entry_point: "spatial_denoise".into(), - zero_initialize_workgroup_memory: false, + shader: load_embedded_asset!(world, "spatial_denoise.wgsl"), + ..default() }); Self { @@ -463,6 +439,8 @@ impl FromWorld for SsaoPipelines { hilbert_index_lut, point_clamp_sampler, linear_clamp_sampler, + + shader: load_embedded_asset!(world, "ssao.wgsl"), } } } @@ -497,11 +475,9 @@ impl SpecializedComputePipeline for SsaoPipelines { self.ssao_bind_group_layout.clone(), self.common_bind_group_layout.clone(), ], - push_constant_ranges: vec![], - shader: SSAO_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "ssao".into(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -553,11 +529,7 @@ fn prepare_ssao_textures( let Some(physical_viewport_size) = camera.physical_viewport_size else { continue; }; - let size = Extent3d { - width: physical_viewport_size.x, - height: physical_viewport_size.y, - depth_or_array_layers: 1, - }; + let size = physical_viewport_size.to_extents(); let preprocessed_depth_texture = texture_cache.get( &render_device, diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index e4cc850d81..6efc3531dd 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -1,14 +1,14 @@ //! Screen space reflections implemented via raymarching. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::{ graph::{Core3d, Node3d}, DEPTH_TEXTURE_SAMPLING_SUPPORTED, }, - fullscreen_vertex_shader, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + FullscreenShader, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -23,10 +23,9 @@ use bevy_ecs::{ }; use bevy_image::BevyDefault as _; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::render_graph::RenderGraph; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{ binding_types, AddressMode, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode, @@ -39,6 +38,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderSystems, }; +use bevy_render::{load_shader_library, render_graph::RenderGraph}; use bevy_utils::{once, prelude::default}; use tracing::info; @@ -49,9 +49,6 @@ use crate::{ ViewLightsUniformOffset, }; -const SSR_SHADER_HANDLE: Handle = weak_handle!("0b559df2-0d61-4f53-bf62-aea16cf32787"); -const RAYMARCH_SHADER_HANDLE: Handle = weak_handle!("798cc6fc-6072-4b6c-ab4f-83905fa4a19e"); - /// Enables screen-space reflections for a camera. /// /// Screen-space reflections are currently only supported with deferred rendering. @@ -158,6 +155,8 @@ pub struct ScreenSpaceReflectionsPipeline { depth_nearest_sampler: Sampler, bind_group_layout: BindGroupLayout, binding_arrays_are_usable: bool, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// A GPU buffer that stores the screen space reflection settings for each view. @@ -179,13 +178,8 @@ pub struct ScreenSpaceReflectionsPipelineKey { impl Plugin for ScreenSpaceReflectionsPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, SSR_SHADER_HANDLE, "ssr.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - RAYMARCH_SHADER_HANDLE, - "raymarch.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "ssr.wgsl"); + load_shader_library!(app, "raymarch.wgsl"); app.register_type::() .add_plugins(ExtractComponentPlugin::::default()); @@ -287,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. @@ -329,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, @@ -339,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(()) @@ -404,6 +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). + fragment_shader: load_embedded_asset!(world, "ssr.wgsl"), } } } @@ -502,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 \ @@ -519,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(), @@ -539,12 +543,11 @@ 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: SSR_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.is_hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -554,12 +557,9 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - push_constant_ranges: vec![], - primitive: default(), - depth_stencil: None, - multisample: default(), - zero_initialize_workgroup_memory: false, + ..default() } } } 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/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index 4af1bbb421..e28412d7bd 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -6,14 +6,14 @@ //! for light beams from directional lights to shine through, creating what is //! known as *light shafts* or *god rays*. //! -//! To add volumetric fog to a scene, add [`VolumetricFog`] to the -//! camera, and add [`VolumetricLight`] to directional lights that you wish to -//! be volumetric. [`VolumetricFog`] feature numerous settings that +//! To add volumetric fog to a scene, add [`crate::VolumetricFog`] to the +//! camera, and add [`crate::VolumetricLight`] to directional lights that you wish to +//! be volumetric. [`crate::VolumetricFog`] feature numerous settings that //! allow you to define the accuracy of the simulation, as well as the look of //! the fog. Currently, only interaction with directional lights that have //! shadow maps is supported. Note that the overhead of the effect scales //! directly with the number of directional lights in use, so apply -//! [`VolumetricLight`] sparingly for the best results. +//! [`crate::VolumetricLight`] sparingly for the best results. //! //! The overall algorithm, which is implemented as a postprocessing effect, is a //! combination of the techniques described in [Scratchapixel] and [this blog @@ -30,34 +30,25 @@ //! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, Assets, Handle}; -use bevy_color::Color; +use bevy_asset::{embedded_asset, Assets, Handle}; use bevy_core_pipeline::core_3d::{ graph::{Core3d, Node3d}, prepare_core_3d_depth_textures, }; -use bevy_ecs::{ - component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs as _, -}; -use bevy_image::Image; +use bevy_ecs::{resource::Resource, schedule::IntoScheduleConfigs as _}; +use bevy_light::FogVolume; use bevy_math::{ primitives::{Cuboid, Plane3d}, Vec2, Vec3, }; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ mesh::{Mesh, Meshable}, - render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{Shader, SpecializedRenderPipelines}, + render_graph::{RenderGraphExt, ViewNodeRunner}, + render_resource::SpecializedRenderPipelines, sync_component::SyncComponentPlugin, - view::Visibility, ExtractSchedule, Render, RenderApp, RenderSystems, }; -use bevy_transform::components::Transform; -use render::{ - VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer, CUBE_MESH, PLANE_MESH, - VOLUMETRIC_FOG_HANDLE, -}; +use render::{VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer}; use crate::graph::NodePbr; @@ -66,142 +57,19 @@ pub mod render; /// A plugin that implements volumetric fog. pub struct VolumetricFogPlugin; -/// Add this component to a [`DirectionalLight`](crate::DirectionalLight) with a shadow map -/// (`shadows_enabled: true`) to make volumetric fog interact with it. -/// -/// This allows the light to generate light shafts/god rays. -#[derive(Clone, Copy, Component, Default, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -pub struct VolumetricLight; - -/// When placed on a [`bevy_core_pipeline::core_3d::Camera3d`], enables -/// volumetric fog and volumetric lighting, also known as light shafts or god -/// rays. -#[derive(Clone, Copy, Component, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -pub struct VolumetricFog { - /// Color of the ambient light. - /// - /// This is separate from Bevy's [`AmbientLight`](crate::light::AmbientLight) because an - /// [`EnvironmentMapLight`](crate::environment_map::EnvironmentMapLight) is - /// still considered an ambient light for the purposes of volumetric fog. If you're using a - /// [`EnvironmentMapLight`](crate::environment_map::EnvironmentMapLight), for best results, - /// this should be a good approximation of the average color of the environment map. - /// - /// Defaults to white. - pub ambient_color: Color, - - /// The brightness of the ambient light. - /// - /// If there's no [`EnvironmentMapLight`](crate::environment_map::EnvironmentMapLight), - /// set this to 0. - /// - /// Defaults to 0.1. - pub ambient_intensity: f32, - - /// The maximum distance to offset the ray origin randomly by, in meters. - /// - /// This is intended for use with temporal antialiasing. It helps fog look - /// less blocky by varying the start position of the ray, using interleaved - /// gradient noise. - pub jitter: f32, - - /// The number of raymarching steps to perform. - /// - /// Higher values produce higher-quality results with less banding, but - /// reduce performance. - /// - /// The default value is 64. - pub step_count: u32, -} - -#[derive(Clone, Component, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -#[require(Transform, Visibility)] -pub struct FogVolume { - /// The color of the fog. - /// - /// Note that the fog must be lit by a [`VolumetricLight`] or ambient light - /// in order for this color to appear. - /// - /// Defaults to white. - pub fog_color: Color, - - /// The density of fog, which measures how dark the fog is. - /// - /// The default value is 0.1. - pub density_factor: f32, - - /// Optional 3D voxel density texture for the fog. - pub density_texture: Option>, - - /// Configurable offset of the density texture in UVW coordinates. - /// - /// This can be used to scroll a repeating density texture in a direction over time - /// to create effects like fog moving in the wind. Make sure to configure the texture - /// to use `ImageAddressMode::Repeat` if this is your intention. - /// - /// Has no effect when no density texture is present. - /// - /// The default value is (0, 0, 0). - pub density_texture_offset: Vec3, - - /// The absorption coefficient, which measures what fraction of light is - /// absorbed by the fog at each step. - /// - /// Increasing this value makes the fog darker. - /// - /// The default value is 0.3. - pub absorption: f32, - - /// The scattering coefficient, which measures the fraction of light that's - /// scattered toward, and away from, the viewer. - /// - /// The default value is 0.3. - pub scattering: f32, - - /// Measures the fraction of light that's scattered *toward* the camera, as - /// opposed to *away* from the camera. - /// - /// Increasing this value makes light shafts become more prominent when the - /// camera is facing toward their source and less prominent when the camera - /// is facing away. Essentially, a high value here means the light shafts - /// will fade into view as the camera focuses on them and fade away when the - /// camera is pointing away. - /// - /// The default value is 0.8. - pub scattering_asymmetry: f32, - - /// Applies a nonphysical color to the light. - /// - /// This can be useful for artistic purposes but is nonphysical. - /// - /// The default value is white. - pub light_tint: Color, - - /// Scales the light by a fixed fraction. - /// - /// This can be useful for artistic purposes but is nonphysical. - /// - /// The default value is 1.0, which results in no adjustment. - pub light_intensity: f32, +#[derive(Resource)] +pub struct FogAssets { + plane_mesh: Handle, + cube_mesh: Handle, } impl Plugin for VolumetricFogPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - VOLUMETRIC_FOG_HANDLE, - "volumetric_fog.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "volumetric_fog.wgsl"); let mut meshes = app.world_mut().resource_mut::>(); - meshes.insert(&PLANE_MESH, Plane3d::new(Vec3::Z, Vec2::ONE).mesh().into()); - meshes.insert(&CUBE_MESH, Cuboid::new(1.0, 1.0, 1.0).mesh().into()); - - app.register_type::() - .register_type::(); + let plane_mesh = meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE).mesh()); + let cube_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()); app.add_plugins(SyncComponentPlugin::::default()); @@ -210,6 +78,10 @@ impl Plugin for VolumetricFogPlugin { }; render_app + .insert_resource(FogAssets { + plane_mesh, + cube_mesh, + }) .init_resource::>() .init_resource::() .add_systems(ExtractSchedule, render::extract_volumetric_fog) @@ -244,31 +116,3 @@ impl Plugin for VolumetricFogPlugin { ); } } - -impl Default for VolumetricFog { - fn default() -> Self { - Self { - step_count: 64, - // Matches `AmbientLight` defaults. - ambient_color: Color::WHITE, - ambient_intensity: 0.1, - jitter: 0.0, - } - } -} - -impl Default for FogVolume { - fn default() -> Self { - Self { - absorption: 0.3, - scattering: 0.3, - density_factor: 0.1, - density_texture: None, - density_texture_offset: Vec3::ZERO, - scattering_asymmetry: 0.5, - fog_color: Color::WHITE, - light_tint: Color::WHITE, - light_intensity: 1.0, - } - } -} diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 07012a72e2..f24550a456 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -2,7 +2,7 @@ use core::array; -use bevy_asset::{weak_handle, AssetId, Handle}; +use bevy_asset::{load_embedded_asset, AssetId, Handle}; use bevy_color::ColorToComponents as _; use bevy_core_pipeline::{ core_3d::Camera3d, @@ -31,10 +31,10 @@ use bevy_render::{ }, BindGroupLayout, BindGroupLayoutEntries, BindingResource, BlendComponent, BlendFactor, BlendOperation, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, - DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, - MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, - RenderPassDescriptor, RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, - ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat, + DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, Operations, + PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, ShaderType, + SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat, TextureSampleType, TextureUsages, VertexState, }, renderer::{RenderContext, RenderDevice, RenderQueue}, @@ -54,6 +54,8 @@ use crate::{ VolumetricLight, }; +use super::FogAssets; + bitflags! { /// Flags that describe the bind group layout used to render volumetric fog. #[derive(Clone, Copy, PartialEq)] @@ -77,24 +79,6 @@ bitflags! { } } -/// The volumetric fog shader. -pub const VOLUMETRIC_FOG_HANDLE: Handle = - weak_handle!("481f474c-2024-44bb-8f79-f7c05ced95ea"); - -/// The plane mesh, which is used to render a fog volume that the camera is -/// inside. -/// -/// This mesh is simply stretched to the size of the framebuffer, as when the -/// camera is inside a fog volume it's essentially a full-screen effect. -pub const PLANE_MESH: Handle = weak_handle!("92523617-c708-4fd0-b42f-ceb4300c930b"); - -/// The cube mesh, which is used to render a fog volume that the camera is -/// outside. -/// -/// Note that only the front faces of this cuboid will be rasterized in -/// hardware. The back faces will be calculated in the shader via raytracing. -pub const CUBE_MESH: Handle = weak_handle!("4a1dd661-2d91-4377-a17a-a914e21e277e"); - /// The total number of bind group layouts. /// /// This is the total number of combinations of all @@ -121,6 +105,9 @@ pub struct VolumetricFogPipeline { /// /// Since there aren't too many of these, we precompile them all. volumetric_view_bind_group_layouts: [BindGroupLayout; VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT], + + // The shader asset handle. + shader: Handle, } /// The two render pipelines that we use for fog volumes: one for when a 3D @@ -266,6 +253,7 @@ impl FromWorld for VolumetricFogPipeline { VolumetricFogPipeline { mesh_view_layouts: mesh_view_layouts.clone(), volumetric_view_bind_group_layouts: bind_group_layouts, + shader: load_embedded_asset!(world, "volumetric_fog.wgsl"), } } } @@ -347,7 +335,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::(); @@ -370,6 +358,7 @@ impl ViewNode for VolumetricFogNode { return Ok(()); }; + let fog_assets = world.resource::(); let render_meshes = world.resource::>(); for view_fog_volume in view_fog_volumes.iter() { @@ -377,9 +366,9 @@ impl ViewNode for VolumetricFogNode { // otherwise, pick the plane mesh. In the latter case we'll be // effectively rendering a full-screen quad. let mesh_handle = if view_fog_volume.exterior { - CUBE_MESH.clone() + fog_assets.cube_mesh.clone() } else { - PLANE_MESH.clone() + fog_assets.plane_mesh.clone() }; let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_handle.id()) @@ -461,7 +450,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 +500,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,26 +544,30 @@ 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], - push_constant_ranges: vec![], + layout, vertex: VertexState { - shader: VOLUMETRIC_FOG_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "vertex".into(), buffers: vec![vertex_format], + ..default() }, primitive: PrimitiveState { cull_mode: Some(Face::Back), ..default() }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: VOLUMETRIC_FOG_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.flags.contains(VolumetricFogPipelineKeyFlags::HDR) { ViewTarget::TEXTURE_FORMAT_HDR @@ -602,8 +591,9 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { }), write_mask: ColorWrites::ALL, })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -614,6 +604,7 @@ pub fn prepare_volumetric_fog_pipelines( pipeline_cache: Res, mut pipelines: ResMut>, volumetric_lighting_pipeline: Res, + fog_assets: Res, view_targets: Query< ( Entity, @@ -628,7 +619,7 @@ pub fn prepare_volumetric_fog_pipelines( >, meshes: Res>, ) { - let Some(plane_mesh) = meshes.get(&PLANE_MESH) else { + let Some(plane_mesh) = meshes.get(&fog_assets.plane_mesh) else { // There's an off chance that the mesh won't be prepared yet if `RenderAssetBytesPerFrame` limiting is in use. return; }; @@ -700,7 +691,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 +703,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/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index 058f73fca9..43e3fc9278 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -251,7 +251,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { // case. let P_uvw = Ro_uvw + Rd_step_uvw * f32(step); if (all(P_uvw >= vec3(0.0)) && all(P_uvw <= vec3(1.0))) { - density *= textureSample(density_texture, density_sampler, P_uvw + density_texture_offset).r; + density *= textureSampleLevel(density_texture, density_sampler, P_uvw + density_texture_offset, 0.0).r; } else { density = 0.0; } diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 7b748c2535..9cf3bc08dd 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -1,10 +1,11 @@ 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::{ - load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, + embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, Assets, Handle, UntypedAssetId, }; use bevy_color::{Color, ColorToComponents}; @@ -37,7 +38,7 @@ use bevy_render::{ render_asset::{ prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_phase::{ AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, @@ -56,9 +57,6 @@ use bevy_render::{ use core::{hash::Hash, ops::Range}; use tracing::error; -pub const WIREFRAME_SHADER_HANDLE: Handle = - weak_handle!("2646a633-f8e3-4380-87ae-b44d881abbce"); - /// A [`Plugin`] that draws wireframes. /// /// Wireframes currently do not work when using webgl or webgpu. @@ -83,12 +81,7 @@ impl WireframePlugin { impl Plugin for WireframePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - WIREFRAME_SHADER_HANDLE, - "render/wireframe.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "render/wireframe.wgsl"); app.add_plugins(( BinnedRenderPhasePlugin::::new(self.debug_flags), @@ -326,7 +319,8 @@ impl RenderCommand

; 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: &Self::State, + state: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -97,7 +112,7 @@ where // SAFETY: We provide the main world on which this system state was initialized on. unsafe { SystemState::

::validate_param( - &state.state, + &mut state.state, main_world.as_unsafe_world_cell_readonly(), ) } diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index 49755f4098..04e4109f09 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -1,25 +1,21 @@ use crate::{ extract_resource::ExtractResource, - prelude::Shader, + load_shader_library, render_resource::{ShaderType, UniformBuffer}, renderer::{RenderDevice, RenderQueue}, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_diagnostic::FrameCount; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; use bevy_time::Time; -pub const GLOBALS_TYPE_HANDLE: Handle = - weak_handle!("9e22a765-30ca-4070-9a4c-34ac08f1c0e7"); - pub struct GlobalsPlugin; impl Plugin for GlobalsPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, GLOBALS_TYPE_HANDLE, "globals.wgsl", Shader::from_wgsl); + load_shader_library!(app, "globals.wgsl"); app.register_type::(); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { 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 bad447bffe..7a2ad06087 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -9,8 +9,8 @@ )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #[cfg(target_pointer_width = "16")] @@ -26,6 +26,7 @@ pub mod alpha; pub mod batching; pub mod camera; pub mod diagnostic; +pub mod erased_render_asset; pub mod experimental; pub mod extract_component; pub mod extract_instances; @@ -37,7 +38,6 @@ pub mod gpu_readback; pub mod mesh; #[cfg(not(target_arch = "wasm32"))] pub mod pipelined_rendering; -pub mod primitives; pub mod render_asset; pub mod render_graph; pub mod render_phase; @@ -49,6 +49,9 @@ pub mod sync_component; pub mod sync_world; pub mod texture; pub mod view; +pub use bevy_camera::primitives; +#[cfg(feature = "bevy_light")] +mod extract_impls; /// The render prelude. /// @@ -57,19 +60,22 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ alpha::AlphaMode, - camera::{ - Camera, ClearColor, ClearColorConfig, OrthographicProjection, PerspectiveProjection, - Projection, - }, + camera::ToNormalizedRenderTarget as _, mesh::{ morph::MorphWeights, primitives::MeshBuilder, primitives::Meshable, Mesh, Mesh2d, Mesh3d, }, render_resource::Shader, - texture::ImagePlugin, + texture::{ImagePlugin, ManualTextureViews}, view::{InheritedVisibility, Msaa, ViewVisibility, Visibility}, ExtractSchedule, }; + // TODO: Remove this in a follow-up + #[doc(hidden)] + pub use bevy_camera::{ + Camera, ClearColor, ClearColorConfig, OrthographicProjection, PerspectiveProjection, + Projection, + }; } use batching::gpu_preprocessing::BatchingPlugin; @@ -79,6 +85,7 @@ pub mod _macro { } use bevy_ecs::schedule::ScheduleBuildSettings; +use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats}; use bevy_utils::prelude::default; pub use extract_param::Extract; @@ -92,7 +99,8 @@ use render_asset::{ use renderer::{RenderAdapter, RenderDevice, RenderQueue}; use settings::RenderResources; use sync_world::{ - despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, SyncWorldPlugin, + despawn_temporary_render_entities, entity_sync_system, MainEntity, RenderEntity, + SyncToRenderWorld, SyncWorldPlugin, TemporaryRenderEntity, }; use crate::gpu_readback::GpuReadbackPlugin; @@ -101,7 +109,7 @@ use crate::{ mesh::{MeshPlugin, MorphPlugin, RenderMesh}, render_asset::prepare_assets, render_resource::{PipelineCache, Shader, ShaderLoader}, - renderer::{render_system, RenderInstance, WgpuWrapper}, + renderer::{render_system, RenderInstance}, settings::RenderCreation, storage::StoragePlugin, view::{ViewPlugin, WindowRenderPlugin}, @@ -110,6 +118,7 @@ use alloc::sync::Arc; use bevy_app::{App, AppLabel, Plugin, SubApp}; use bevy_asset::{AssetApp, AssetServer}; use bevy_ecs::{prelude::*, schedule::ScheduleLabel}; +use bevy_utils::WgpuWrapper; use bitflags::bitflags; use core::ops::{Deref, DerefMut}; use std::sync::Mutex; @@ -217,6 +226,10 @@ pub enum RenderSystems { #[deprecated(since = "0.17.0", note = "Renamed to `RenderSystems`.")] pub type RenderSet = RenderSystems; +/// The startup schedule of the [`RenderApp`] +#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct RenderStartup; + /// The main render schedule. #[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] pub struct Render; @@ -349,10 +362,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 }, }, }); @@ -373,10 +388,19 @@ impl Plugin for RenderPlugin { } }); + let force_fallback_adapter = std::env::var("WGPU_FORCE_FALLBACK_ADAPTER") + .map_or(settings.force_fallback_adapter, |v| { + !(v.is_empty() || v == "0" || v == "false") + }); + + let desired_adapter_name = std::env::var("WGPU_ADAPTER_NAME") + .as_deref() + .map_or(settings.adapter_name.clone(), |x| Some(x.to_lowercase())); + let request_adapter_options = wgpu::RequestAdapterOptions { power_preference: settings.power_preference, compatible_surface: surface.as_ref(), - ..Default::default() + force_fallback_adapter, }; let (device, queue, adapter_info, render_adapter) = @@ -384,6 +408,7 @@ impl Plugin for RenderPlugin { &instance, &settings, &request_adapter_options, + desired_adapter_name, ) .await; debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); @@ -445,10 +470,9 @@ impl Plugin for RenderPlugin { app.register_type::() // These types cannot be registered in bevy_color, as it does not depend on the rest of Bevy .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() + .register_type::() + .register_type::() + .register_type::() .register_type::(); } @@ -469,10 +493,15 @@ impl Plugin for RenderPlugin { let RenderResources(device, queue, adapter_info, render_adapter, instance) = future_render_resources.0.lock().unwrap().take().unwrap(); + let compressed_image_format_support = CompressedImageFormatSupport( + CompressedImageFormats::from_features(device.features()), + ); + app.insert_resource(device.clone()) .insert_resource(queue.clone()) .insert_resource(adapter_info.clone()) - .insert_resource(render_adapter.clone()); + .insert_resource(render_adapter.clone()) + .insert_resource(compressed_image_format_support); let render_app = app.sub_app_mut(RenderApp); @@ -547,7 +576,19 @@ unsafe fn initialize_render_app(app: &mut App) { ), ); - render_app.set_extract(|main_world, render_world| { + // We want the closure to have a flag to only run the RenderStartup schedule once, but the only + // way to have the closure store this flag is by capturing it. This variable is otherwise + // unused. + let mut should_run_startup = true; + render_app.set_extract(move |main_world, render_world| { + if should_run_startup { + // Run the `RenderStartup` if it hasn't run yet. This does mean `RenderStartup` blocks + // the rest of the app extraction, but this is necessary since extraction itself can + // depend on resources initialized in `RenderStartup`. + render_world.run_schedule(RenderStartup); + should_run_startup = false; + } + { #[cfg(feature = "trace")] let _stage_span = tracing::info_span!("entity_sync").entered(); diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index b492dd6bb2..d1e35523dc 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -104,17 +104,11 @@ fn project_onto(lhs: vec3, rhs: vec3) -> vec3 { // are likely most useful when raymarching, for example, where complete numeric // accuracy can be sacrificed for greater sample count. -fn fast_sqrt(x: f32) -> f32 { - let n = bitcast(0x1fbd1df5 + (bitcast(x) >> 1u)); - // One Newton's method iteration for better precision - return 0.5 * (n + x / n); -} - // Slightly less accurate than fast_acos_4, but much simpler. fn fast_acos(in_x: f32) -> f32 { let x = abs(in_x); var res = -0.156583 * x + HALF_PI; - res *= fast_sqrt(1.0 - x); + res *= sqrt(1.0 - x); return select(PI - res, res, in_x >= 0.0); } @@ -131,7 +125,7 @@ fn fast_acos_4(x: f32) -> f32 { s = -0.2121144 * x1 + 1.5707288; s = 0.0742610 * x2 + s; s = -0.0187293 * x3 + s; - s = fast_sqrt(1.0 - x1) * s; + s = sqrt(1.0 - x1) * s; // acos function mirroring return select(PI - s, s, x >= 0.0); diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index eb2d4de626..bbdb543116 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -78,6 +78,9 @@ pub struct MeshAllocator { /// WebGL 2. On this platform, we must give each vertex array its own /// buffer, because we can't adjust the first vertex when we perform a draw. general_vertex_slabs_supported: bool, + + /// Additional buffer usages to add to any vertex or index buffers created. + pub extra_buffer_usages: BufferUsages, } /// Tunable parameters that customize the behavior of the allocator. @@ -348,6 +351,7 @@ impl FromWorld for MeshAllocator { mesh_id_to_index_slab: HashMap::default(), next_slab_id: default(), general_vertex_slabs_supported, + extra_buffer_usages: BufferUsages::empty(), } } } @@ -448,13 +452,17 @@ impl MeshAllocator { // Allocate. for (mesh_id, mesh) in &extracted_meshes.extracted { + let vertex_buffer_size = mesh.get_vertex_buffer_size() as u64; + if vertex_buffer_size == 0 { + continue; + } // Allocate vertex data. Note that we can only pack mesh vertex data // together if the platform supports it. let vertex_element_layout = ElementLayout::vertex(mesh_vertex_buffer_layouts, mesh); if self.general_vertex_slabs_supported { self.allocate( mesh_id, - mesh.get_vertex_buffer_size() as u64, + vertex_buffer_size, vertex_element_layout, &mut slabs_to_grow, mesh_allocator_settings, @@ -598,7 +606,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: len as u64, - usage: buffer_usages | BufferUsages::COPY_DST, + usage: buffer_usages | BufferUsages::COPY_DST | self.extra_buffer_usages, mapped_at_creation: true, }); { @@ -835,7 +843,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: slab.current_slot_capacity as u64 * slab.element_layout.slot_size(), - usage: buffer_usages, + usage: buffer_usages | self.extra_buffer_usages, mapped_at_creation: false, }); diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index d15468376f..f7aa593a3a 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -1,14 +1,11 @@ -use bevy_math::Vec3; +use bevy_camera::visibility::VisibilitySystems; pub use bevy_mesh::*; use morph::{MeshMorphWeights, MorphWeights}; pub mod allocator; -mod components; use crate::{ - primitives::Aabb, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_resource::TextureView, texture::GpuImage, - view::VisibilitySystems, RenderApp, }; use allocator::MeshAllocatorPlugin; @@ -21,7 +18,7 @@ use bevy_ecs::{ SystemParamItem, }, }; -pub use components::{mark_3d_meshes_as_changed_if_their_assets_changed, Mesh2d, Mesh3d, MeshTag}; +pub use bevy_mesh::{mark_3d_meshes_as_changed_if_their_assets_changed, Mesh2d, Mesh3d, MeshTag}; use wgpu::IndexFormat; /// Registers all [`MeshBuilder`] types. @@ -112,26 +109,6 @@ pub fn inherit_weights( } } -pub trait MeshAabb { - /// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space - /// - /// Returns `None` if `self` doesn't have [`Mesh::ATTRIBUTE_POSITION`] of - /// type [`VertexAttributeValues::Float32x3`], or if `self` doesn't have any vertices. - fn compute_aabb(&self) -> Option; -} - -impl MeshAabb for Mesh { - fn compute_aabb(&self) -> Option { - let Some(VertexAttributeValues::Float32x3(values)) = - self.attribute(Mesh::ATTRIBUTE_POSITION) - else { - return None; - }; - - Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p))) - } -} - /// The render world representation of a [`Mesh`]. #[derive(Debug, Clone)] pub struct RenderMesh { @@ -209,6 +186,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/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index efe728e7c7..00dfc4ba0e 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -197,7 +197,7 @@ fn renderer_extract(app_world: &mut World, _world: &mut World) { render_channels.send_blocking(render_app); } else { // Renderer thread panicked - world.send_event(AppExit::error()); + world.write_event(AppExit::error()); } }); }); diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 1fa5758ca3..c35062eb85 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); @@ -497,14 +499,14 @@ impl RenderAssetBytesPerFrameLimiter { } /// Decreases the available bytes for the current frame. - fn write_bytes(&self, bytes: usize) { + pub(crate) fn write_bytes(&self, bytes: usize) { if self.max_bytes.is_some() && bytes > 0 { self.bytes_written.fetch_add(bytes, Ordering::Relaxed); } } /// Returns `true` if there are no remaining bytes available for writing this frame. - fn exhausted(&self) -> bool { + pub(crate) fn exhausted(&self) -> bool { if let Some(max_bytes) = self.max_bytes { let bytes_written = self.bytes_written.load(Ordering::Relaxed); bytes_written >= max_bytes diff --git a/crates/bevy_render/src/render_graph/app.rs b/crates/bevy_render/src/render_graph/app.rs index 338ae75d7a..fce6a13ad3 100644 --- a/crates/bevy_render/src/render_graph/app.rs +++ b/crates/bevy_render/src/render_graph/app.rs @@ -1,11 +1,11 @@ use bevy_app::{App, SubApp}; -use bevy_ecs::world::FromWorld; +use bevy_ecs::world::{FromWorld, World}; use tracing::warn; use super::{IntoRenderNodeArray, Node, RenderGraph, RenderLabel, RenderSubGraph}; /// Adds common [`RenderGraph`] operations to [`SubApp`] (and [`App`]). -pub trait RenderGraphApp { +pub trait RenderGraphExt { // Add a sub graph to the [`RenderGraph`] fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self; /// Add a [`Node`] to the [`RenderGraph`]: @@ -32,15 +32,15 @@ pub trait RenderGraphApp { ) -> &mut Self; } -impl RenderGraphApp for SubApp { +impl RenderGraphExt for World { fn add_render_graph_node( &mut self, sub_graph: impl RenderSubGraph, node_label: impl RenderLabel, ) -> &mut Self { let sub_graph = sub_graph.intern(); - let node = T::from_world(self.world_mut()); - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let node = T::from_world(self); + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_node on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { @@ -59,7 +59,7 @@ impl RenderGraphApp for SubApp { edges: impl IntoRenderNodeArray, ) -> &mut Self { let sub_graph = sub_graph.intern(); - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_edges on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { @@ -79,7 +79,7 @@ impl RenderGraphApp for SubApp { input_node: impl RenderLabel, ) -> &mut Self { let sub_graph = sub_graph.intern(); - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_edge on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { @@ -93,7 +93,7 @@ impl RenderGraphApp for SubApp { } fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_sub_graph on the RenderApp", ); render_graph.add_sub_graph(sub_graph, RenderGraph::default()); @@ -101,13 +101,13 @@ impl RenderGraphApp for SubApp { } } -impl RenderGraphApp for App { +impl RenderGraphExt for SubApp { fn add_render_graph_node( &mut self, sub_graph: impl RenderSubGraph, node_label: impl RenderLabel, ) -> &mut Self { - SubApp::add_render_graph_node::(self.main_mut(), sub_graph, node_label); + World::add_render_graph_node::(self.world_mut(), sub_graph, node_label); self } @@ -117,7 +117,7 @@ impl RenderGraphApp for App { output_node: impl RenderLabel, input_node: impl RenderLabel, ) -> &mut Self { - SubApp::add_render_graph_edge(self.main_mut(), sub_graph, output_node, input_node); + World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node); self } @@ -126,12 +126,47 @@ impl RenderGraphApp for App { sub_graph: impl RenderSubGraph, edges: impl IntoRenderNodeArray, ) -> &mut Self { - SubApp::add_render_graph_edges(self.main_mut(), sub_graph, edges); + World::add_render_graph_edges(self.world_mut(), sub_graph, edges); self } fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { - SubApp::add_render_sub_graph(self.main_mut(), sub_graph); + World::add_render_sub_graph(self.world_mut(), sub_graph); + self + } +} + +impl RenderGraphExt for App { + fn add_render_graph_node( + &mut self, + sub_graph: impl RenderSubGraph, + node_label: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_node::(self.world_mut(), sub_graph, node_label); + self + } + + fn add_render_graph_edge( + &mut self, + sub_graph: impl RenderSubGraph, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node); + self + } + + fn add_render_graph_edges( + &mut self, + sub_graph: impl RenderSubGraph, + edges: impl IntoRenderNodeArray, + ) -> &mut Self { + World::add_render_graph_edges(self.world_mut(), sub_graph, edges); + self + } + + fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { + World::add_render_sub_graph(self.world_mut(), sub_graph); self } } diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/render_graph/camera_driver_node.rs similarity index 97% rename from crates/bevy_render/src/camera/camera_driver_node.rs rename to crates/bevy_render/src/render_graph/camera_driver_node.rs index 8be5a345b4..d18c7d1133 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/render_graph/camera_driver_node.rs @@ -1,9 +1,10 @@ use crate::{ - camera::{ClearColor, ExtractedCamera, NormalizedRenderTarget, SortedCameras}, + camera::{ExtractedCamera, NormalizedRenderTarget, SortedCameras}, render_graph::{Node, NodeRunError, RenderGraphContext}, renderer::RenderContext, view::ExtractedWindows, }; +use bevy_camera::ClearColor; use bevy_ecs::{entity::ContainsEntity, prelude::QueryState, world::World}; use bevy_platform::collections::HashSet; use wgpu::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp}; diff --git a/crates/bevy_render/src/render_graph/mod.rs b/crates/bevy_render/src/render_graph/mod.rs index fbed33a23c..6f98a30f4b 100644 --- a/crates/bevy_render/src/render_graph/mod.rs +++ b/crates/bevy_render/src/render_graph/mod.rs @@ -1,4 +1,5 @@ mod app; +mod camera_driver_node; mod context; mod edge; mod graph; @@ -6,6 +7,7 @@ mod node; mod node_slot; pub use app::*; +pub use camera_driver_node::*; pub use context::*; pub use edge::*; pub use graph::*; 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 a12d336018..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 { @@ -315,7 +317,6 @@ where /// Prepares the render command to be used. This is called once and only once before the phase /// begins. There may be zero or more [`draw`](RenderCommandState::draw) calls following a call to this function. fn prepare(&mut self, world: &'_ World) { - self.state.update_archetypes(world); self.view.update_archetypes(world); self.entity.update_archetypes(world); } @@ -328,7 +329,7 @@ where view: Entity, item: &P, ) -> Result<(), DrawError> { - let param = self.state.get_manual(world); + let param = self.state.get(world); let view = match self.view.get_manual(world, view) { Ok(view) => view, Err(err) => match err { diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index a7b8acdc00..2024535504 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -1,5 +1,4 @@ use crate::{ - camera::Viewport, diagnostic::internal::{Pass, PassKind, WritePipelineStatistics, WriteTimestamp}, render_resource::{ BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId, @@ -7,6 +6,7 @@ use crate::{ }, renderer::RenderDevice, }; +use bevy_camera::Viewport; use bevy_color::LinearRgba; use bevy_utils::default; use core::ops::Range; diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 272418f67f..c58318f654 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -61,7 +61,9 @@ use crate::{ render_resource::{CachedRenderPipelineId, GpuArrayBufferIndex, PipelineCache}, Render, RenderApp, RenderSystems, }; +use bevy_ecs::intern::Interned; use bevy_ecs::{ + define_label, prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; @@ -69,6 +71,33 @@ use core::{fmt::Debug, hash::Hash, iter, marker::PhantomData, ops::Range, slice: use smallvec::SmallVec; use tracing::warn; +pub use bevy_render_macros::ShaderLabel; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(ShaderLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + ShaderLabel, + SHADER_LABEL_INTERNER +); + +/// A shorthand for `Interned`. +pub type InternedShaderLabel = Interned; + +pub use bevy_render_macros::DrawFunctionLabel; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(DrawFunctionLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + DrawFunctionLabel, + DRAW_FUNCTION_LABEL_INTERNER +); + +pub type InternedDrawFunctionLabel = Interned; + /// Stores the rendering instructions for a single phase that uses bins in all /// views. /// diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 2c8e984bfd..04b7747179 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -1,4 +1,3 @@ -use crate::renderer::WgpuWrapper; use crate::{ define_atomic_id, render_asset::RenderAssets, @@ -9,6 +8,7 @@ use crate::{ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::system::{SystemParam, SystemParamItem}; pub use bevy_render_macros::AsBindGroup; +use bevy_utils::WgpuWrapper; use core::ops::Deref; use encase::ShaderType; use thiserror::Error; @@ -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` @@ -481,22 +481,27 @@ impl Deref for BindGroup { /// is_shaded: bool, /// } /// -/// #[derive(Copy, Clone, Hash, Eq, PartialEq)] +/// // Materials keys are intended to be small, cheap to hash, and +/// // uniquely identify a specific material permutation, which +/// // is why they are required to be `bytemuck::Pod` and `bytemuck::Zeroable` +/// // when using the `AsBindGroup` derive macro. +/// #[repr(C)] +/// #[derive(Copy, Clone, Hash, Eq, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] /// struct CoolMaterialKey { -/// is_shaded: bool, +/// is_shaded: u32, /// } /// /// impl From<&CoolMaterial> for CoolMaterialKey { /// fn from(material: &CoolMaterial) -> CoolMaterialKey { /// CoolMaterialKey { -/// is_shaded: material.is_shaded, +/// is_shaded: material.is_shaded as u32, /// } /// } /// } /// ``` pub trait AsBindGroup { /// Data that will be stored alongside the "prepared" bind group. - type Data: Send + Sync; + type Data: bytemuck::Pod + bytemuck::Zeroable + Send + Sync; type Param: SystemParam + 'static; @@ -531,8 +536,8 @@ pub trait AsBindGroup { layout: &BindGroupLayout, render_device: &RenderDevice, param: &mut SystemParamItem<'_, '_, Self::Param>, - ) -> Result, AsBindGroupError> { - let UnpreparedBindGroup { bindings, data } = + ) -> Result { + let UnpreparedBindGroup { bindings } = Self::unprepared_bind_group(self, layout, render_device, param, false)?; let entries = bindings @@ -548,10 +553,11 @@ pub trait AsBindGroup { Ok(PreparedBindGroup { bindings, bind_group, - data, }) } + fn bind_group_data(&self) -> Self::Data; + /// Returns a vec of (binding index, `OwnedBindingResource`). /// /// In cases where `OwnedBindingResource` is not available (as for bindless @@ -569,7 +575,7 @@ pub trait AsBindGroup { render_device: &RenderDevice, param: &mut SystemParamItem<'_, '_, Self::Param>, force_no_bindless: bool, - ) -> Result, AsBindGroupError>; + ) -> Result; /// Creates the bind group layout matching all bind groups returned by /// [`AsBindGroup::as_bind_group`] @@ -613,16 +619,14 @@ pub enum AsBindGroupError { } /// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`]. -pub struct PreparedBindGroup { +pub struct PreparedBindGroup { pub bindings: BindingResources, pub bind_group: BindGroup, - pub data: T, } /// a map containing `OwnedBindingResource`s, keyed by the target binding index -pub struct UnpreparedBindGroup { +pub struct UnpreparedBindGroup { pub bindings: BindingResources, - pub data: T, } /// A pair of binding index and binding resource, used as part of 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 3aaf46183f..847bb46f49 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -147,6 +147,13 @@ impl<'a> IntoBinding<'a> for &'a TextureView { } } +impl<'a> IntoBinding<'a> for &'a wgpu::TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -161,6 +168,13 @@ impl<'a> IntoBinding<'a> for &'a Sampler { } } +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::SamplerArray(self) + } +} + impl<'a> IntoBinding<'a> for BindingResource<'a> { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -175,6 +189,13 @@ impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { } } +impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::BufferArray(self) + } +} + pub trait IntoBindingArray<'b, const N: usize> { fn into_array(self) -> [BindingResource<'b>; N]; } @@ -266,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.rs b/crates/bevy_render/src/render_resource/bind_group_layout.rs index e19f5b969f..2d674f46d1 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout.rs @@ -1,5 +1,5 @@ use crate::define_atomic_id; -use crate::renderer::WgpuWrapper; +use bevy_utils::WgpuWrapper; use core::ops::Deref; define_atomic_id!(BindGroupLayoutId); 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 bc4a7d306d..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, @@ -568,4 +575,18 @@ pub mod binding_types { } .into_bind_group_layout_entry_builder() } + + pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { + 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.rs b/crates/bevy_render/src/render_resource/buffer.rs index 9b7bb2c41f..b779417bf5 100644 --- a/crates/bevy_render/src/render_resource/buffer.rs +++ b/crates/bevy_render/src/render_resource/buffer.rs @@ -1,5 +1,5 @@ use crate::define_atomic_id; -use crate::renderer::WgpuWrapper; +use bevy_utils::WgpuWrapper; use core::ops::{Bound, Deref, RangeBounds}; define_atomic_id!(BufferId); 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 b777d96290..f156b0ecb0 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -12,6 +12,7 @@ mod pipeline_cache; mod pipeline_specializer; pub mod resource_macros; mod shader; +mod specializer; mod storage_buffer; mod texture; mod uniform_buffer; @@ -28,6 +29,7 @@ pub use pipeline::*; pub use pipeline_cache::*; pub use pipeline_specializer::*; pub use shader::*; +pub use specializer::*; pub use storage_buffer::*; pub use texture::*; pub use uniform_buffer::*; @@ -38,27 +40,31 @@ pub use wgpu::{ BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder, }, - AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, - BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, + AccelerationStructureFlags, AccelerationStructureGeometryFlags, + AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, + AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, Blas, BlasBuildEntry, BlasGeometries, + BlasGeometrySizeDescriptors, BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, - DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, - FilterMode, FragmentState as RawFragmentState, FrontFace, ImageSubresourceRange, IndexFormat, - Limits as WgpuLimits, LoadOp, Maintain, 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, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, - VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, + CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, MapMode, + MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, + 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, }; pub use crate::mesh::VertexBufferLayout; diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index 30f9a974b8..e94cf27cd3 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -1,12 +1,12 @@ use super::ShaderDefVal; use crate::mesh::VertexBufferLayout; -use crate::renderer::WgpuWrapper; use crate::{ define_atomic_id, render_resource::{BindGroupLayout, Shader}, }; use alloc::borrow::Cow; use bevy_asset::Handle; +use bevy_utils::WgpuWrapper; use core::ops::Deref; use wgpu::{ ColorTargetState, DepthStencilState, MultisampleState, PrimitiveState, PushConstantRange, @@ -88,7 +88,7 @@ impl Deref for ComputePipeline { } /// Describes a render (graphics) pipeline. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Default)] pub struct RenderPipelineDescriptor { /// Debug label of the pipeline. This will show up in graphics debuggers for easy identification. pub label: Option>, @@ -112,33 +112,33 @@ pub struct RenderPipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct VertexState { /// The compiled shader module for this stage. pub shader: Handle, pub shader_defs: Vec, - /// The name of the entry point in the compiled shader. There must be a - /// function with this name in the shader. - pub entry_point: Cow<'static, str>, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, /// The format of any vertex buffers used with this pipeline. pub buffers: Vec, } /// Describes the fragment process in a render pipeline. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct FragmentState { /// The compiled shader module for this stage. pub shader: Handle, pub shader_defs: Vec, - /// The name of the entry point in the compiled shader. There must be a - /// function with this name in the shader. - pub entry_point: Cow<'static, str>, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, /// The color state of the render targets. pub targets: Vec>, } /// Describes a compute pipeline. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct ComputePipelineDescriptor { pub label: Option>, pub layout: Vec, @@ -146,9 +146,9 @@ pub struct ComputePipelineDescriptor { /// The compiled shader module for this stage. pub shader: Handle, pub shader_defs: Vec, - /// The name of the entry point in the compiled shader. There must be a - /// function with this name in the shader. - pub entry_point: Cow<'static, str>, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, /// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true. /// If this is false, reading from workgroup variables before writing to them will result in garbage values. pub zero_initialize_workgroup_memory: bool, diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 7c54b0f406..328c5e5600 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -1,4 +1,3 @@ -use crate::renderer::WgpuWrapper; use crate::{ render_resource::*, renderer::{RenderAdapter, RenderDevice}, @@ -14,7 +13,8 @@ use bevy_ecs::{ use bevy_platform::collections::{hash_map::EntryRef, HashMap, HashSet}; use bevy_tasks::Task; use bevy_utils::default; -use core::{future::Future, hash::Hash, mem, ops::Deref}; +use bevy_utils::WgpuWrapper; +use core::{future::Future, hash::Hash, mem}; use naga::valid::Capabilities; use std::sync::{Mutex, PoisonError}; use thiserror::Error; @@ -80,9 +80,12 @@ pub struct CachedPipeline { } /// State of a cached pipeline inserted into a [`PipelineCache`]. -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) )] #[derive(Debug)] pub enum CachedPipelineState { @@ -866,14 +869,14 @@ impl PipelineCache { let fragment_data = descriptor.fragment.as_ref().map(|fragment| { ( fragment_module.unwrap(), - fragment.entry_point.deref(), + fragment.entry_point.as_deref(), fragment.targets.as_slice(), ) }); // TODO: Expose the rest of this somehow let compilation_options = PipelineCompilationOptions { - constants: &default(), + constants: &[], zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory, }; @@ -886,7 +889,7 @@ impl PipelineCache { primitive: descriptor.primitive, vertex: RawVertexState { buffers: &vertex_buffer_layouts, - entry_point: Some(descriptor.vertex.entry_point.deref()), + entry_point: descriptor.vertex.entry_point.as_deref(), module: &vertex_module, // TODO: Should this be the same as the fragment compilation options? compilation_options: compilation_options.clone(), @@ -894,7 +897,7 @@ impl PipelineCache { fragment: fragment_data .as_ref() .map(|(module, entry_point, targets)| RawFragmentState { - entry_point: Some(entry_point), + entry_point: entry_point.as_deref(), module, targets, // TODO: Should this be the same as the vertex compilation options? @@ -952,10 +955,10 @@ impl PipelineCache { label: descriptor.label.as_deref(), layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }), module: &compute_module, - entry_point: Some(&descriptor.entry_point), + entry_point: descriptor.entry_point.as_deref(), // TODO: Expose the rest of this somehow compilation_options: PipelineCompilationOptions { - constants: &default(), + constants: &[], zero_initialize_workgroup_memory: descriptor .zero_initialize_workgroup_memory, }, @@ -1103,10 +1106,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, @@ -1118,9 +1117,12 @@ fn create_pipeline_task( } /// Type of error returned by a [`PipelineCache`] when the creation of a GPU pipeline object failed. -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) )] #[derive(Error, Debug)] pub enum PipelineCacheError { @@ -1159,8 +1161,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 +1203,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 +1239,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/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs new file mode 100644 index 0000000000..d7a2f3aca1 --- /dev/null +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -0,0 +1,465 @@ +use super::{ + CachedComputePipelineId, CachedRenderPipelineId, ComputePipeline, ComputePipelineDescriptor, + PipelineCache, RenderPipeline, RenderPipelineDescriptor, +}; +use bevy_ecs::{ + error::BevyError, + resource::Resource, + world::{FromWorld, World}, +}; +use bevy_platform::{ + collections::{ + hash_map::{Entry, VacantEntry}, + HashMap, + }, + hash::FixedHasher, +}; +use core::{hash::Hash, marker::PhantomData}; +use tracing::error; +use variadics_please::all_tuples; + +pub use bevy_render_macros::{Specializer, SpecializerKey}; + +/// Defines a type that is able to be "specialized" and cached by creating and transforming +/// its descriptor type. This is implemented for [`RenderPipeline`] and [`ComputePipeline`], and +/// likely will not have much utility for other types. +/// +/// See docs on [`Specializer`] for more info. +pub trait Specializable { + type Descriptor: PartialEq + Clone + Send + Sync; + type CachedId: Clone + Send + Sync; + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId; + fn get_descriptor(pipeline_cache: &PipelineCache, id: Self::CachedId) -> &Self::Descriptor; +} + +impl Specializable for RenderPipeline { + type Descriptor = RenderPipelineDescriptor; + type CachedId = CachedRenderPipelineId; + + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId { + pipeline_cache.queue_render_pipeline(descriptor) + } + + fn get_descriptor( + pipeline_cache: &PipelineCache, + id: CachedRenderPipelineId, + ) -> &Self::Descriptor { + pipeline_cache.get_render_pipeline_descriptor(id) + } +} + +impl Specializable for ComputePipeline { + type Descriptor = ComputePipelineDescriptor; + + type CachedId = CachedComputePipelineId; + + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId { + pipeline_cache.queue_compute_pipeline(descriptor) + } + + fn get_descriptor( + pipeline_cache: &PipelineCache, + id: CachedComputePipelineId, + ) -> &Self::Descriptor { + pipeline_cache.get_compute_pipeline_descriptor(id) + } +} + +/// Defines a type capable of "specializing" values of a type T. +/// +/// Specialization is the process of generating variants of a type T +/// from small hashable keys, and specializers themselves can be +/// thought of as [pure functions] from the key type to `T`, that +/// [memoize] their results based on the key. +/// +///

+/// +/// Since compiling render and compute pipelines can be so slow, +/// specialization allows a Bevy app to detect when it would compile +/// a duplicate pipeline and reuse what's already in the cache. While +/// pipelines could all be memoized hashing each whole descriptor, this +/// would be much slower and could still create duplicates. In contrast, +/// memoizing groups of *related* pipelines based on a small hashable +/// key is much faster. See the docs on [`SpecializerKey`] for more info. +/// +/// ## Composing Specializers +/// +/// This trait can be derived with `#[derive(Specializer)]` for structs whose +/// fields all implement [`Specializer`]. This allows for composing multiple +/// specializers together, and makes encapsulation and separating concerns +/// between specializers much nicer. One could make individual specializers +/// for common operations and place them in entirely separate modules, then +/// compose them together with a single `#[derive]` +/// +/// ```rust +/// # use bevy_ecs::error::BevyError; +/// # use bevy_render::render_resource::Specializer; +/// # use bevy_render::render_resource::SpecializerKey; +/// # use bevy_render::render_resource::RenderPipeline; +/// # use bevy_render::render_resource::RenderPipelineDescriptor; +/// struct A; +/// struct B; +/// #[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)] +/// struct BKey { contrived_number: u32 }; +/// +/// impl Specializer for A { +/// type Key = (); +/// +/// fn specialize( +/// &self, +/// key: (), +/// descriptor: &mut RenderPipelineDescriptor +/// ) -> Result<(), BevyError> { +/// # let _ = descriptor; +/// // mutate the descriptor here +/// Ok(key) +/// } +/// } +/// +/// impl Specializer for B { +/// type Key = BKey; +/// +/// fn specialize( +/// &self, +/// key: BKey, +/// descriptor: &mut RenderPipelineDescriptor +/// ) -> Result { +/// # let _ = descriptor; +/// // mutate the descriptor here +/// Ok(key) +/// } +/// } +/// +/// #[derive(Specializer)] +/// #[specialize(RenderPipeline)] +/// struct C { +/// #[key(default)] +/// a: A, +/// b: B, +/// } +/// +/// /* +/// The generated implementation: +/// impl Specializer for C { +/// type Key = BKey; +/// fn specialize( +/// &self, +/// key: Self::Key, +/// descriptor: &mut RenderPipelineDescriptor +/// ) -> Result, BevyError> { +/// let _ = self.a.specialize((), descriptor); +/// let key = self.b.specialize(key, descriptor); +/// Ok(key) +/// } +/// } +/// */ +/// ``` +/// +/// The key type for a composed specializer will be a tuple of the keys +/// of each field, and their specialization logic will be applied in field +/// order. Since derive macros can't have generic parameters, the derive macro +/// requires an additional `#[specialize(..targets)]` attribute to specify a +/// list of types to target for the implementation. `#[specialize(all)]` is +/// also allowed, and will generate a fully generic implementation at the cost +/// of slightly worse error messages. +/// +/// Additionally, each field can optionally take a `#[key]` attribute to +/// specify a "key override". This will hide that field's key from being +/// exposed by the wrapper, and always use the value given by the attribute. +/// Values for this attribute may either be `default` which will use the key's +/// [`Default`] implementation, or a valid rust expression of the key type. +/// +/// [pure functions]: https://en.wikipedia.org/wiki/Pure_function +/// [memoize]: https://en.wikipedia.org/wiki/Memoization +pub trait Specializer: Send + Sync + 'static { + type Key: SpecializerKey; + fn specialize( + &self, + key: Self::Key, + descriptor: &mut T::Descriptor, + ) -> Result, BevyError>; +} + +// TODO: update docs for `SpecializerKey` with a more concrete example +// once we've migrated mesh layout specialization + +/// Defines a type that is able to be used as a key for [`Specializer`]s +/// +///
+/// Most types should implement this trait with the included derive macro.
+/// This generates a "canonical" key type, with IS_CANONICAL = true, and Canonical = Self +///
+/// +/// ## What's a "canonical" key? +/// +/// The specialization API memoizes pipelines based on the hash of each key, but this +/// can still produce duplicates. For example, if one used a list of vertex attributes +/// as a key, even if all the same attributes were present they could be in any order. +/// In each case, though the keys would be "different" they would produce the same +/// pipeline. +/// +/// To address this, during specialization keys are processed into a [canonical] +/// (or "standard") form that represents the actual descriptor that was produced. +/// In the previous example, that would be the final `VertexBufferLayout` contained +/// by the pipeline descriptor. This new key is used by [`SpecializedCache`] to +/// perform additional checks for duplicates, but only if required. If a key is +/// canonical from the start, then there's no need. +/// +/// For implementors: the main property of a canonical key is that if two keys hash +/// differently, they should nearly always produce different descriptors. +/// +/// [canonical]: https://en.wikipedia.org/wiki/Canonicalization +pub trait SpecializerKey: Clone + Hash + Eq { + /// Denotes whether this key is canonical or not. This should only be `true` + /// if and only if `Canonical = Self`. + const IS_CANONICAL: bool; + + /// The canonical key type to convert this into during specialization. + type Canonical: Hash + Eq; +} + +pub type Canonical = ::Canonical; + +impl Specializer for () { + type Key = (); + + fn specialize( + &self, + _key: Self::Key, + _descriptor: &mut T::Descriptor, + ) -> Result<(), BevyError> { + Ok(()) + } +} + +impl Specializer for PhantomData { + type Key = (); + + fn specialize( + &self, + _key: Self::Key, + _descriptor: &mut T::Descriptor, + ) -> Result<(), BevyError> { + Ok(()) + } +} + +macro_rules! impl_specialization_key_tuple { + ($($T:ident),*) => { + impl <$($T: SpecializerKey),*> SpecializerKey for ($($T,)*) { + const IS_CANONICAL: bool = true $(&& <$T as SpecializerKey>::IS_CANONICAL)*; + type Canonical = ($(Canonical<$T>,)*); + } + }; +} + +// TODO: How to we fake_variadics this? +all_tuples!(impl_specialization_key_tuple, 0, 12, T); + +/// Defines a specializer that can also provide a "base descriptor". +/// +/// In order to be composable, [`Specializer`] implementers don't create full +/// descriptors, only transform them. However, [`SpecializedCache`]s need a +/// "base descriptor" at creation time in order to have something for the +/// [`Specializer`] to work off of. This trait allows [`SpecializedCache`] +/// to impl [`FromWorld`] for [`Specializer`]s that also satisfy [`FromWorld`] +/// and [`GetBaseDescriptor`]. +/// +/// This trait can be also derived with `#[derive(Specializer)]`, by marking +/// a field with `#[base_descriptor]` to use its [`GetBaseDescriptor`] implementation. +/// +/// Example: +/// ```rust +/// # use bevy_ecs::error::BevyError; +/// # use bevy_render::render_resource::Specializer; +/// # use bevy_render::render_resource::GetBaseDescriptor; +/// # use bevy_render::render_resource::SpecializerKey; +/// # use bevy_render::render_resource::RenderPipeline; +/// # use bevy_render::render_resource::RenderPipelineDescriptor; +/// struct A; +/// struct B; +/// +/// impl Specializer for A { +/// # type Key = (); +/// # +/// # fn specialize( +/// # &self, +/// # key: (), +/// # _descriptor: &mut RenderPipelineDescriptor +/// # ) -> Result<(), BevyError> { +/// # Ok(key) +/// # } +/// // ... +/// } +/// +/// impl Specializer for B { +/// # type Key = (); +/// # +/// # fn specialize( +/// # &self, +/// # key: (), +/// # _descriptor: &mut RenderPipelineDescriptor +/// # ) -> Result<(), BevyError> { +/// # Ok(key) +/// # } +/// // ... +/// } +/// +/// impl GetBaseDescriptor for B { +/// fn get_base_descriptor(&self) -> RenderPipelineDescriptor { +/// # todo!() +/// // ... +/// } +/// } +/// +/// +/// #[derive(Specializer)] +/// #[specialize(RenderPipeline)] +/// struct C { +/// a: A, +/// #[base_descriptor] +/// b: B, +/// } +/// +/// /* +/// The generated implementation: +/// impl GetBaseDescriptor for C { +/// fn get_base_descriptor(&self) -> RenderPipelineDescriptor { +/// self.b.base_descriptor() +/// } +/// } +/// */ +/// ``` +pub trait GetBaseDescriptor: Specializer { + fn get_base_descriptor(&self) -> T::Descriptor; +} + +pub type SpecializerFn = + fn(>::Key, &mut ::Descriptor) -> Result<(), BevyError>; + +/// A cache for specializable resources. For a given key type the resulting +/// resource will only be created if it is missing, retrieving it from the +/// cache otherwise. +#[derive(Resource)] +pub struct SpecializedCache> { + specializer: S, + user_specializer: Option>, + base_descriptor: T::Descriptor, + primary_cache: HashMap, + secondary_cache: HashMap, T::CachedId>, +} + +impl> SpecializedCache { + /// Creates a new [`SpecializedCache`] from a [`Specializer`], + /// an optional "user specializer", and a base descriptor. The + /// user specializer is applied after the [`Specializer`], with + /// the same key. + #[inline] + pub fn new( + specializer: S, + user_specializer: Option>, + base_descriptor: T::Descriptor, + ) -> Self { + Self { + specializer, + user_specializer, + base_descriptor, + primary_cache: Default::default(), + secondary_cache: Default::default(), + } + } + + /// Specializes a resource given the [`Specializer`]'s key type. + #[inline] + pub fn specialize( + &mut self, + pipeline_cache: &PipelineCache, + key: S::Key, + ) -> Result { + let entry = self.primary_cache.entry(key.clone()); + match entry { + Entry::Occupied(entry) => Ok(entry.get().clone()), + Entry::Vacant(entry) => Self::specialize_slow( + &self.specializer, + self.user_specializer, + self.base_descriptor.clone(), + pipeline_cache, + key, + entry, + &mut self.secondary_cache, + ), + } + } + + #[cold] + fn specialize_slow( + specializer: &S, + user_specializer: Option>, + base_descriptor: T::Descriptor, + pipeline_cache: &PipelineCache, + key: S::Key, + primary_entry: VacantEntry, + secondary_cache: &mut HashMap, T::CachedId>, + ) -> Result { + let mut descriptor = base_descriptor.clone(); + let canonical_key = specializer.specialize(key.clone(), &mut descriptor)?; + + if let Some(user_specializer) = user_specializer { + (user_specializer)(key, &mut descriptor)?; + } + + // if the whole key is canonical, the secondary cache isn't needed. + if ::IS_CANONICAL { + return Ok(primary_entry + .insert(::queue(pipeline_cache, descriptor)) + .clone()); + } + + let id = match secondary_cache.entry(canonical_key) { + Entry::Occupied(entry) => { + if cfg!(debug_assertions) { + let stored_descriptor = + ::get_descriptor(pipeline_cache, entry.get().clone()); + if &descriptor != stored_descriptor { + error!( + "Invalid Specializer<{}> impl for {}: the cached descriptor \ + is not equal to the generated descriptor for the given key. \ + This means the Specializer implementation uses unused information \ + from the key to specialize the pipeline. This is not allowed \ + because it would invalidate the cache.", + core::any::type_name::(), + core::any::type_name::() + ); + } + } + entry.into_mut().clone() + } + Entry::Vacant(entry) => entry + .insert(::queue(pipeline_cache, descriptor)) + .clone(), + }; + + primary_entry.insert(id.clone()); + Ok(id) + } +} + +/// [`SpecializedCache`] implements [`FromWorld`] for [`Specializer`]s +/// that also satisfy [`FromWorld`] and [`GetBaseDescriptor`]. This will +/// create a [`SpecializedCache`] with no user specializer, and the base +/// descriptor take from the specializer's [`GetBaseDescriptor`] implementation. +impl FromWorld for SpecializedCache +where + T: Specializable, + S: FromWorld + Specializer + GetBaseDescriptor, +{ + fn from_world(world: &mut World) -> Self { + let specializer = S::from_world(world); + let base_descriptor = specializer.get_base_descriptor(); + Self::new(specializer, None, base_descriptor) + } +} diff --git a/crates/bevy_render/src/render_resource/texture.rs b/crates/bevy_render/src/render_resource/texture.rs index f975fc18f3..c96da8a1be 100644 --- a/crates/bevy_render/src/render_resource/texture.rs +++ b/crates/bevy_render/src/render_resource/texture.rs @@ -1,7 +1,7 @@ use crate::define_atomic_id; -use crate::renderer::WgpuWrapper; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::resource::Resource; +use bevy_utils::WgpuWrapper; use core::ops::Deref; define_atomic_id!(TextureId); diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 1691911c2c..7cb8023de1 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -4,9 +4,10 @@ mod render_device; use bevy_derive::{Deref, DerefMut}; #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] use bevy_tasks::ComputeTaskPool; +use bevy_utils::WgpuWrapper; pub use graph_runner::*; pub use render_device::*; -use tracing::{error, info, info_span, warn}; +use tracing::{debug, error, info, info_span, trace, warn}; use crate::{ diagnostic::{internal::DiagnosticsRecorder, RecordDiagnostics}, @@ -22,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. @@ -120,46 +121,6 @@ pub fn render_system(world: &mut World, state: &mut SystemState(T); -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -#[derive(Debug, Clone, Deref, DerefMut)] -pub struct WgpuWrapper(send_wrapper::SendWrapper); - -// SAFETY: SendWrapper is always Send + Sync. -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -unsafe impl Send for WgpuWrapper {} -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -unsafe impl Sync for WgpuWrapper {} - -#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] -impl WgpuWrapper { - pub fn new(t: T) -> Self { - Self(t) - } - - pub fn into_inner(self) -> T { - self.0 - } -} - -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -impl WgpuWrapper { - pub fn new(t: T) -> Self { - Self(send_wrapper::SendWrapper::new(t)) - } - - pub fn into_inner(self) -> T { - self.0.take() - } -} - /// This queue is used to enqueue tasks for the GPU to execute asynchronously. #[derive(Resource, Clone, Deref, DerefMut)] pub struct RenderQueue(pub Arc>); @@ -190,19 +151,47 @@ pub async fn initialize_renderer( instance: &Instance, options: &WgpuSettings, request_adapter_options: &RequestAdapterOptions<'_, '_>, + desired_adapter_name: Option, ) -> (RenderDevice, RenderQueue, RenderAdapterInfo, RenderAdapter) { - let adapter = instance - .request_adapter(request_adapter_options) - .await - .expect(GPU_NOT_FOUND_ERROR_MESSAGE); + let mut selected_adapter = None; + if let Some(adapter_name) = &desired_adapter_name { + debug!("Searching for adapter with name: {}", adapter_name); + for adapter in instance.enumerate_adapters(options.backends.expect( + "The `backends` field of `WgpuSettings` must be set to use a specific adapter.", + )) { + trace!("Checking adapter: {:?}", adapter.get_info()); + let info = adapter.get_info(); + if let Some(surface) = request_adapter_options.compatible_surface { + if !adapter.is_surface_supported(surface) { + continue; + } + } + if info + .name + .to_lowercase() + .contains(&adapter_name.to_lowercase()) + { + selected_adapter = Some(adapter); + break; + } + } + } else { + debug!( + "Searching for adapter with options: {:?}", + request_adapter_options + ); + selected_adapter = instance.request_adapter(request_adapter_options).await.ok(); + }; + + let adapter = selected_adapter.expect(GPU_NOT_FOUND_ERROR_MESSAGE); let adapter_info = adapter.get_info(); info!("{:?}", adapter_info); if adapter_info.device_type == DeviceType::Cpu { warn!( "The selected adapter is using a driver that only supports software rendering. \ - This is likely to be very slow. See https://bevyengine.org/learn/errors/b0006/" + This is likely to be very slow. See https://bevy.org/learn/errors/b0006/" ); } @@ -216,21 +205,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; @@ -279,6 +262,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), @@ -349,15 +338,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 d33139745b..b1a20d2ace 100644 --- a/crates/bevy_render/src/renderer/render_device.rs +++ b/crates/bevy_render/src/renderer/render_device.rs @@ -3,11 +3,11 @@ use crate::render_resource::{ BindGroup, BindGroupLayout, Buffer, ComputePipeline, RawRenderPipelineDescriptor, RenderPipeline, Sampler, Texture, }; -use crate::WgpuWrapper; 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..d4456953af 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,10 @@ 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, + /// If true, will force wgpu to use a software renderer, if available. + pub force_fallback_adapter: bool, + /// The name of the adapter to use. + pub adapter_name: Option, } impl Default for WgpuSettings { @@ -114,6 +116,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 +140,8 @@ impl Default for WgpuSettings { gles3_minor_version, instance_flags, memory_hints: MemoryHints::default(), - trace_path: None, + force_fallback_adapter: false, + adapter_name: 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 f15f3c4003..b216b5fd32 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,15 +1,16 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; +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, system::{Local, Query, ResMut, SystemState}, - world::{Mut, OnAdd, OnRemove, World}, + world::{Mut, World}, }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -93,12 +94,12 @@ impl Plugin for SyncWorldPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); app.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |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()) { @@ -126,8 +127,9 @@ pub struct SyncToRenderWorld; /// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity. /// /// Can also be used as a newtype wrapper for render world entities. -#[derive(Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, Component)] +#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, Reflect)] #[component(clone_behavior = Ignore)] +#[reflect(Component, Clone)] pub struct RenderEntity(Entity); impl RenderEntity { #[inline] @@ -154,7 +156,8 @@ unsafe impl EntityEquivalent for RenderEntity {} /// Component added on the render world entities to keep track of the corresponding main world entity. /// /// Can also be used as a newtype wrapper for main world entities. -#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Reflect)] +#[reflect(Component, Clone)] pub struct MainEntity(Entity); impl MainEntity { #[inline] @@ -217,10 +220,10 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl EntityRecord::Added(e) => { if let Ok(mut main_entity) = world.get_entity_mut(e) { match main_entity.entry::() { - bevy_ecs::world::Entry::Occupied(_) => { + bevy_ecs::world::ComponentEntry::Occupied(_) => { panic!("Attempting to synchronize an entity that has already been synchronized!"); } - bevy_ecs::world::Entry::Vacant(entry) => { + bevy_ecs::world::ComponentEntry::Vacant(entry) => { let id = render_world.spawn(MainEntity(e)).id(); entry.insert(RenderEntity(id)); @@ -278,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}, }; @@ -296,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> { @@ -311,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, ) { @@ -324,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`. @@ -361,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() } } @@ -383,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 { @@ -396,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> { @@ -411,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, @@ -424,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`. @@ -461,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)] @@ -488,10 +510,11 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - observer::Trigger, + lifecycle::{Add, Remove}, + observer::On, query::With, system::{Query, ResMut}, - world::{OnAdd, OnRemove, World}, + world::World, }; use super::{ @@ -509,12 +532,12 @@ mod tests { main_world.init_resource::(); main_world.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |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()) { diff --git a/crates/bevy_render/src/texture/gpu_image.rs b/crates/bevy_render/src/texture/gpu_image.rs index 551bd3ee02..6fbc9dfea7 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,17 +45,48 @@ 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( render_queue, &image.texture_descriptor, - // TODO: Is this correct? Do we need to use `MipMajor` if it's a ktx2 file? - wgpu::util::TextureDataOrder::default(), + image.data_order, 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/camera/manual_texture_view.rs b/crates/bevy_render/src/texture/manual_texture_view.rs similarity index 67% rename from crates/bevy_render/src/camera/manual_texture_view.rs rename to crates/bevy_render/src/texture/manual_texture_view.rs index 56eff5612a..b291463c29 100644 --- a/crates/bevy_render/src/camera/manual_texture_view.rs +++ b/crates/bevy_render/src/texture/manual_texture_view.rs @@ -1,15 +1,12 @@ -use crate::{extract_resource::ExtractResource, render_resource::TextureView}; -use bevy_ecs::{prelude::Component, reflect::ReflectComponent, resource::Resource}; -use bevy_image::BevyDefault as _; +use bevy_camera::ManualTextureViewHandle; +use bevy_ecs::{prelude::Component, resource::Resource}; +use bevy_image::BevyDefault; use bevy_math::UVec2; use bevy_platform::collections::HashMap; -use bevy_reflect::prelude::*; +use bevy_render_macros::ExtractResource; use wgpu::TextureFormat; -/// A unique id that corresponds to a specific [`ManualTextureView`] in the [`ManualTextureViews`] collection. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Component, Reflect)] -#[reflect(Component, Default, Debug, PartialEq, Hash, Clone)] -pub struct ManualTextureViewHandle(pub u32); +use crate::render_resource::TextureView; /// A manually managed [`TextureView`] for use as a [`crate::camera::RenderTarget`]. #[derive(Debug, Clone, Component)] diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index de5361aad8..78cc78a9a1 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -1,25 +1,32 @@ mod fallback_image; mod gpu_image; +mod manual_texture_view; 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; -use bevy_image::{CompressedImageFormats, Image, ImageLoader, ImageSamplerDescriptor}; +use bevy_image::{ + CompressedImageFormatSupport, CompressedImageFormats, Image, ImageLoader, + ImageSamplerDescriptor, +}; pub use fallback_image::*; pub use gpu_image::*; +pub use manual_texture_view::*; pub use texture_attachment::*; pub use texture_cache::*; use crate::{ - render_asset::RenderAssetPlugin, renderer::RenderDevice, Render, RenderApp, RenderSystems, + extract_resource::ExtractResourcePlugin, render_asset::RenderAssetPlugin, + renderer::RenderDevice, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; -use bevy_asset::{weak_handle, AssetApp, Assets, Handle}; +use bevy_asset::{uuid_handle, AssetApp, Assets, Handle}; use bevy_ecs::prelude::*; +use tracing::warn; /// A handle to a 1 x 1 transparent white image. /// @@ -27,7 +34,7 @@ use bevy_ecs::prelude::*; /// While that handle points to an opaque white 1 x 1 image, this handle points to a transparent 1 x 1 white image. // Number randomly selected by fair WolframAlpha query. Totally arbitrary. pub const TRANSPARENT_IMAGE_HANDLE: Handle = - weak_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46"); + uuid_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46"); // TODO: replace Texture names with Image names? /// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU. @@ -70,17 +77,21 @@ impl Plugin for ImagePlugin { app.init_asset_loader::(); } - app.add_plugins(RenderAssetPlugin::::default()) - .register_type::() - .init_asset::() - .register_asset_reflect::(); + app.add_plugins(( + RenderAssetPlugin::::default(), + ExtractResourcePlugin::::default(), + )) + .init_resource::() + .register_type::() + .init_asset::() + .register_asset_reflect::(); let mut image_assets = app.world_mut().resource_mut::>(); 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::() @@ -111,12 +122,16 @@ impl Plugin for ImagePlugin { fn finish(&self, app: &mut App) { if !ImageLoader::SUPPORTED_FORMATS.is_empty() { - let supported_compressed_formats = match app.world().get_resource::() { - Some(render_device) => { - CompressedImageFormats::from_features(render_device.features()) - } - None => CompressedImageFormats::NONE, + let supported_compressed_formats = if let Some(resource) = + app.world().get_resource::() + { + resource.0 + } else { + warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ + RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); + CompressedImageFormats::NONE }; + app.register_asset_loader(ImageLoader::new(supported_compressed_formats)); } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 2f80e5f94b..cdda04cf2d 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -1,28 +1,26 @@ pub mod visibility; pub mod window; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_camera::{ + primitives::Frustum, CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure, +}; use bevy_diagnostic::FrameCount; pub use visibility::*; pub use window::*; use crate::{ - camera::{ - CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure, ExtractedCamera, - ManualTextureViews, MipBias, NormalizedRenderTarget, TemporalJitter, - }, + camera::{ExtractedCamera, MipBias, NormalizedRenderTarget, TemporalJitter}, experimental::occlusion_culling::OcclusionCulling, extract_component::ExtractComponentPlugin, - prelude::Shader, - primitives::Frustum, + load_shader_library, render_asset::RenderAssets, render_phase::ViewRangefinder3d, render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, sync_world::MainEntity, texture::{ - CachedTexture, ColorAttachment, DepthAttachment, GpuImage, OutputColorAttachment, - TextureCache, + CachedTexture, ColorAttachment, DepthAttachment, GpuImage, ManualTextureViews, + OutputColorAttachment, TextureCache, }, Render, RenderApp, RenderSystems, }; @@ -31,7 +29,7 @@ use bevy_app::{App, Plugin}; use bevy_color::LinearRgba; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; -use bevy_image::BevyDefault as _; +use bevy_image::{BevyDefault as _, ToExtents}; use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_platform::collections::{hash_map::Entry, HashMap}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -42,12 +40,10 @@ use core::{ sync::atomic::{AtomicUsize, Ordering}, }; use wgpu::{ - BufferUsages, Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, + BufferUsages, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, }; -pub const VIEW_TYPE_HANDLE: Handle = weak_handle!("7234423c-38bb-411c-acec-f67730f6db5b"); - /// The matrix that converts from the RGB to the LMS color space. /// /// To derive this, first we convert from RGB to [CIE 1931 XYZ]: @@ -101,23 +97,17 @@ pub struct ViewPlugin; impl Plugin for ViewPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, VIEW_TYPE_HANDLE, "view.wgsl", Shader::from_wgsl); + load_shader_library!(app, "view.wgsl"); - app.register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() + app.register_type::() .register_type::() .register_type::() // NOTE: windows.is_changed() handles cases where a window was resized .add_plugins(( + ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), - VisibilityPlugin, - VisibilityRangePlugin, + RenderVisibilityRangePlugin, )); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { @@ -194,11 +184,21 @@ impl Msaa { 2 => Msaa::Sample2, 4 => Msaa::Sample4, 8 => Msaa::Sample8, - _ => panic!("Unsupported MSAA sample count: {}", samples), + _ => panic!("Unsupported MSAA sample count: {samples}"), } } } +/// If this component is added to a camera, the camera will use an intermediate "high dynamic range" render texture. +/// This allows rendering with a wider range of lighting values. However, this does *not* affect +/// whether the camera will render with hdr display output (which bevy does not support currently) +/// and only affects the intermediate render texture. +#[derive( + Component, Default, Copy, Clone, ExtractComponent, Reflect, PartialEq, Eq, Hash, Debug, +)] +#[reflect(Component, Default, PartialEq, Hash, Debug)] +pub struct Hdr; + /// An identifier for a view that is stable across frames. /// /// We can't use [`Entity`] for this because render world entities aren't @@ -262,34 +262,36 @@ impl RetainedViewEntity { pub struct ExtractedView { /// The entity in the main world corresponding to this render world view. pub retained_view_entity: RetainedViewEntity, - /// Typically a right-handed projection matrix, one of either: + /// Typically a column-major right-handed projection matrix, one of either: /// /// Perspective (infinite reverse z) /// ```text /// f = 1 / tan(fov_y_radians / 2) /// - /// ⎡ f / aspect 0 0 0 ⎤ - /// ⎢ 0 f 0 0 ⎥ - /// ⎢ 0 0 0 -1 ⎥ - /// ⎣ 0 0 near 0 ⎦ + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ /// ``` /// /// Orthographic /// ```text /// w = right - left /// h = top - bottom - /// d = near - far + /// d = far - near /// cw = -right - left /// ch = -top - bottom /// - /// ⎡ 2 / w 0 0 0 ⎤ - /// ⎢ 0 2 / h 0 0 ⎥ - /// ⎢ 0 0 1 / d 0 ⎥ - /// ⎣ cw / w ch / h near / d 1 ⎦ + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ /// ``` /// /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// /// Custom projections are also possible however. pub clip_from_view: Mat4, pub world_from_view: GlobalTransform, @@ -306,7 +308,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()) } } @@ -529,34 +531,36 @@ pub struct ViewUniform { pub world_from_clip: Mat4, pub world_from_view: Mat4, pub view_from_world: Mat4, - /// Typically a right-handed projection matrix, one of either: + /// Typically a column-major right-handed projection matrix, one of either: /// /// Perspective (infinite reverse z) /// ```text /// f = 1 / tan(fov_y_radians / 2) /// - /// ⎡ f / aspect 0 0 0 ⎤ - /// ⎢ 0 f 0 0 ⎥ - /// ⎢ 0 0 0 -1 ⎥ - /// ⎣ 0 0 near 0 ⎦ + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ /// ``` /// /// Orthographic /// ```text /// w = right - left /// h = top - bottom - /// d = near - far + /// d = far - near /// cw = -right - left /// ch = -top - bottom /// - /// ⎡ 2 / w 0 0 0 ⎤ - /// ⎢ 0 2 / h 0 0 ⎥ - /// ⎢ 0 0 1 / d 0 ⎥ - /// ⎣ cw / w ch / h near / d 1 ⎦ + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ /// ``` /// /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// /// Custom projections are also possible however. pub clip_from_view: Mat4, pub view_from_clip: Mat4, @@ -717,9 +721,6 @@ impl From for ColorGradingUniform { #[derive(Component, Default)] pub struct NoIndirectDrawing; -#[derive(Component, Default)] -pub struct NoCpuCulling; - impl ViewTarget { pub const TEXTURE_FORMAT_HDR: TextureFormat = TextureFormat::Rgba16Float; @@ -922,7 +923,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() { @@ -1034,12 +1035,6 @@ pub fn prepare_view_targets( continue; }; - let size = Extent3d { - width: target_size.x, - height: target_size.y, - depth_or_array_layers: 1, - }; - let main_texture_format = if view.hdr { ViewTarget::TEXTURE_FORMAT_HDR } else { @@ -1057,7 +1052,7 @@ pub fn prepare_view_targets( .or_insert_with(|| { let descriptor = TextureDescriptor { label: None, - size, + size: target_size.to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -1088,7 +1083,7 @@ pub fn prepare_view_targets( &render_device, TextureDescriptor { label: Some("main_texture_sampled"), - size, + size: target_size.to_extents(), mip_level_count: 1, sample_count: msaa.samples(), dimension: TextureDimension::D2, diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 317de2eb88..7b14bab9e1 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -19,33 +19,35 @@ struct View { world_from_clip: mat4x4, world_from_view: mat4x4, view_from_world: mat4x4, - // Typically a right-handed projection matrix, one of either: + // Typically a column-major right-handed projection matrix, one of either: // // Perspective (infinite reverse z) // ``` // f = 1 / tan(fov_y_radians / 2) // - // ⎡ f / aspect 0 0 0 ⎤ - // ⎢ 0 f 0 0 ⎥ - // ⎢ 0 0 0 -1 ⎥ - // ⎣ 0 0 near 0 ⎦ + // ⎡ f / aspect 0 0 0 ⎤ + // ⎢ 0 f 0 0 ⎥ + // ⎢ 0 0 0 near ⎥ + // ⎣ 0 0 -1 0 ⎦ // ``` // // Orthographic // ``` // w = right - left // h = top - bottom - // d = near - far + // d = far - near // cw = -right - left // ch = -top - bottom // - // ⎡ 2 / w 0 0 0 ⎤ - // ⎢ 0 2 / h 0 0 ⎥ - // ⎢ 0 0 1 / d 0 ⎥ - // ⎣ cw / w ch / h near / d 1 ⎦ + // ⎡ 2 / w 0 0 cw / w ⎤ + // ⎢ 0 2 / h 0 ch / h ⎥ + // ⎢ 0 0 1 / d far / d ⎥ + // ⎣ 0 0 0 1 ⎦ // ``` // // `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + // + // Wgsl matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` // // Custom projections are also possible however. clip_from_view: mat4x4, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 3a0772b687..281b6967da 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -1,269 +1,14 @@ -mod range; -mod render_layers; - use core::any::TypeId; -use bevy_ecs::component::HookContext; -use bevy_ecs::entity::EntityHashSet; -use bevy_ecs::world::DeferredWorld; -use derive_more::derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_utils::TypeIdMap; + +use crate::sync_world::MainEntity; + +mod range; +pub use bevy_camera::visibility::*; pub use range::*; -pub use render_layers::*; - -use bevy_app::{Plugin, PostUpdate}; -use bevy_asset::Assets; -use bevy_ecs::{hierarchy::validate_parent_has_component, prelude::*}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_transform::{components::GlobalTransform, TransformSystems}; -use bevy_utils::{Parallel, TypeIdMap}; -use smallvec::SmallVec; - -use super::NoCpuCulling; -use crate::{ - camera::{Camera, CameraProjection, Projection}, - mesh::{Mesh, Mesh3d, MeshAabb}, - primitives::{Aabb, Frustum, Sphere}, - sync_world::MainEntity, -}; - -/// User indication of whether an entity is visible. Propagates down the entity hierarchy. -/// -/// If an entity is hidden in this way, all [`Children`] (and all of their children and so on) who -/// are set to [`Inherited`](Self::Inherited) will also be hidden. -/// -/// This is done by the `visibility_propagate_system` which uses the entity hierarchy and -/// `Visibility` to set the values of each entity's [`InheritedVisibility`] component. -#[derive(Component, Clone, Copy, Reflect, Debug, PartialEq, Eq, Default)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] -#[require(InheritedVisibility, ViewVisibility)] -pub enum Visibility { - /// An entity with `Visibility::Inherited` will inherit the Visibility of its [`ChildOf`] target. - /// - /// A root-level entity that is set to `Inherited` will be visible. - #[default] - Inherited, - /// An entity with `Visibility::Hidden` will be unconditionally hidden. - Hidden, - /// An entity with `Visibility::Visible` will be unconditionally visible. - /// - /// Note that an entity with `Visibility::Visible` will be visible regardless of whether the - /// [`ChildOf`] target entity is hidden. - Visible, -} - -impl Visibility { - /// Toggles between `Visibility::Inherited` and `Visibility::Visible`. - /// If the value is `Visibility::Hidden`, it remains unaffected. - #[inline] - pub fn toggle_inherited_visible(&mut self) { - *self = match *self { - Visibility::Inherited => Visibility::Visible, - Visibility::Visible => Visibility::Inherited, - _ => *self, - }; - } - /// Toggles between `Visibility::Inherited` and `Visibility::Hidden`. - /// If the value is `Visibility::Visible`, it remains unaffected. - #[inline] - pub fn toggle_inherited_hidden(&mut self) { - *self = match *self { - Visibility::Inherited => Visibility::Hidden, - Visibility::Hidden => Visibility::Inherited, - _ => *self, - }; - } - /// Toggles between `Visibility::Visible` and `Visibility::Hidden`. - /// If the value is `Visibility::Inherited`, it remains unaffected. - #[inline] - pub fn toggle_visible_hidden(&mut self) { - *self = match *self { - Visibility::Visible => Visibility::Hidden, - Visibility::Hidden => Visibility::Visible, - _ => *self, - }; - } -} - -// Allows `&Visibility == Visibility` -impl PartialEq for &Visibility { - #[inline] - fn eq(&self, other: &Visibility) -> bool { - // Use the base Visibility == Visibility implementation. - >::eq(*self, other) - } -} - -// Allows `Visibility == &Visibility` -impl PartialEq<&Visibility> for Visibility { - #[inline] - fn eq(&self, other: &&Visibility) -> bool { - // Use the base Visibility == Visibility implementation. - >::eq(self, *other) - } -} - -/// Whether or not an entity is visible in the hierarchy. -/// This will not be accurate until [`VisibilityPropagate`] runs in the [`PostUpdate`] schedule. -/// -/// If this is false, then [`ViewVisibility`] should also be false. -/// -/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate -#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] -#[component(on_insert = validate_parent_has_component::)] -pub struct InheritedVisibility(bool); - -impl InheritedVisibility { - /// An entity that is invisible in the hierarchy. - pub const HIDDEN: Self = Self(false); - /// An entity that is visible in the hierarchy. - pub const VISIBLE: Self = Self(true); - - /// Returns `true` if the entity is visible in the hierarchy. - /// Otherwise, returns `false`. - #[inline] - pub fn get(self) -> bool { - self.0 - } -} - -/// A bucket into which we group entities for the purposes of visibility. -/// -/// Bevy's various rendering subsystems (3D, 2D, UI, etc.) want to be able to -/// quickly winnow the set of entities to only those that the subsystem is -/// tasked with rendering, to avoid spending time examining irrelevant entities. -/// At the same time, Bevy wants the [`check_visibility`] system to determine -/// all entities' visibilities at the same time, regardless of what rendering -/// subsystem is responsible for drawing them. Additionally, your application -/// may want to add more types of renderable objects that Bevy determines -/// visibility for just as it does for Bevy's built-in objects. -/// -/// The solution to this problem is *visibility classes*. A visibility class is -/// a type, typically the type of a component, that represents the subsystem -/// that renders it: for example, `Mesh3d`, `Mesh2d`, and `Sprite`. The -/// [`VisibilityClass`] component stores the visibility class or classes that -/// the entity belongs to. (Generally, an object will belong to only one -/// visibility class, but in rare cases it may belong to multiple.) -/// -/// When adding a new renderable component, you'll typically want to write an -/// add-component hook that adds the type ID of that component to the -/// [`VisibilityClass`] array. See `custom_phase_item` for an example. -// -// Note: This can't be a `ComponentId` because the visibility classes are copied -// into the render world, and component IDs are per-world. -#[derive(Clone, Component, Default, Reflect, Deref, DerefMut)] -#[reflect(Component, Default, Clone)] -pub struct VisibilityClass(pub SmallVec<[TypeId; 1]>); - -/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering. -/// -/// Each frame, this will be reset to `false` during [`VisibilityPropagate`] systems in [`PostUpdate`]. -/// Later in the frame, systems in [`CheckVisibility`] will mark any visible entities using [`ViewVisibility::set`]. -/// Because of this, values of this type will be marked as changed every frame, even when they do not change. -/// -/// If you wish to add custom visibility system that sets this value, make sure you add it to the [`CheckVisibility`] set. -/// -/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate -/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility -#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)] -#[reflect(Component, Default, Debug, PartialEq, Clone)] -pub struct ViewVisibility(bool); - -impl ViewVisibility { - /// An entity that cannot be seen from any views. - pub const HIDDEN: Self = Self(false); - - /// Returns `true` if the entity is visible in any view. - /// Otherwise, returns `false`. - #[inline] - pub fn get(self) -> bool { - self.0 - } - - /// Sets the visibility to `true`. This should not be considered reversible for a given frame, - /// as this component tracks whether or not the entity visible in _any_ view. - /// - /// This will be automatically reset to `false` every frame in [`VisibilityPropagate`] and then set - /// to the proper value in [`CheckVisibility`]. - /// - /// You should only manually set this if you are defining a custom visibility system, - /// in which case the system should be placed in the [`CheckVisibility`] set. - /// For normal user-defined entity visibility, see [`Visibility`]. - /// - /// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate - /// [`CheckVisibility`]: VisibilitySystems::CheckVisibility - #[inline] - pub fn set(&mut self) { - self.0 = true; - } -} - -/// Use this component to opt-out of built-in frustum culling for entities, see -/// [`Frustum`]. -/// -/// It can be used for example: -/// - when a [`Mesh`] is updated but its [`Aabb`] is not, which might happen with animations, -/// - when using some light effects, like wanting a [`Mesh`] out of the [`Frustum`] -/// to appear in the reflection of a [`Mesh`] within. -#[derive(Debug, Component, Default, Reflect)] -#[reflect(Component, Default, Debug)] -pub struct NoFrustumCulling; - -/// Collection of entities visible from the current view. -/// -/// This component contains all entities which are visible from the currently -/// rendered view. The collection is updated automatically by the [`VisibilitySystems::CheckVisibility`] -/// system set. Renderers can use the equivalent [`RenderVisibleEntities`] to optimize rendering of -/// a particular view, to prevent drawing items not visible from that view. -/// -/// This component is intended to be attached to the same entity as the [`Camera`] and -/// the [`Frustum`] defining the view. -#[derive(Clone, Component, Default, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -pub struct VisibleEntities { - #[reflect(ignore, clone)] - pub entities: TypeIdMap>, -} - -impl VisibleEntities { - pub fn get(&self, type_id: TypeId) -> &[Entity] { - match self.entities.get(&type_id) { - Some(entities) => &entities[..], - None => &[], - } - } - - pub fn get_mut(&mut self, type_id: TypeId) -> &mut Vec { - self.entities.entry(type_id).or_default() - } - - pub fn iter(&self, type_id: TypeId) -> impl DoubleEndedIterator { - self.get(type_id).iter() - } - - pub fn len(&self, type_id: TypeId) -> usize { - self.get(type_id).len() - } - - pub fn is_empty(&self, type_id: TypeId) -> bool { - self.get(type_id).is_empty() - } - - pub fn clear(&mut self, type_id: TypeId) { - self.get_mut(type_id).clear(); - } - - pub fn clear_all(&mut self) { - // Don't just nuke the hash table; we want to reuse allocations. - for entities in self.entities.values_mut() { - entities.clear(); - } - } - - pub fn push(&mut self, entity: Entity, type_id: TypeId) { - self.get_mut(type_id).push(entity); - } -} /// Collection of entities visible from the current view. /// @@ -307,667 +52,3 @@ impl RenderVisibleEntities { self.get::().is_empty() } } - -#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum VisibilitySystems { - /// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems, - /// calculating and inserting an [`Aabb`] to relevant entities. - CalculateBounds, - /// Label for [`update_frusta`] in [`CameraProjectionPlugin`](crate::camera::CameraProjectionPlugin). - UpdateFrusta, - /// Label for the system propagating the [`InheritedVisibility`] in a - /// [`ChildOf`] / [`Children`] hierarchy. - VisibilityPropagate, - /// Label for the [`check_visibility`] system updating [`ViewVisibility`] - /// of each entity and the [`VisibleEntities`] of each view.\ - /// - /// System order ambiguities between systems in this set are ignored: - /// the order of systems within this set is irrelevant, as [`check_visibility`] - /// assumes that its operations are irreversible during the frame. - CheckVisibility, - /// Label for the `mark_newly_hidden_entities_invisible` system, which sets - /// [`ViewVisibility`] to [`ViewVisibility::HIDDEN`] for entities that no - /// view has marked as visible. - MarkNewlyHiddenEntitiesInvisible, -} - -pub struct VisibilityPlugin; - -impl Plugin for VisibilityPlugin { - fn build(&self, app: &mut bevy_app::App) { - use VisibilitySystems::*; - - app.register_type::() - .configure_sets( - PostUpdate, - (CalculateBounds, UpdateFrusta, VisibilityPropagate) - .before(CheckVisibility) - .after(TransformSystems::Propagate), - ) - .configure_sets( - PostUpdate, - MarkNewlyHiddenEntitiesInvisible.after(CheckVisibility), - ) - .init_resource::() - .add_systems( - PostUpdate, - ( - calculate_bounds.in_set(CalculateBounds), - (visibility_propagate_system, reset_view_visibility) - .in_set(VisibilityPropagate), - check_visibility.in_set(CheckVisibility), - mark_newly_hidden_entities_invisible.in_set(MarkNewlyHiddenEntitiesInvisible), - ), - ); - } -} - -/// Computes and adds an [`Aabb`] component to entities with a -/// [`Mesh3d`] component and without a [`NoFrustumCulling`] component. -/// -/// This system is used in system set [`VisibilitySystems::CalculateBounds`]. -pub fn calculate_bounds( - mut commands: Commands, - meshes: Res>, - without_aabb: Query<(Entity, &Mesh3d), (Without, Without)>, -) { - for (entity, mesh_handle) in &without_aabb { - if let Some(mesh) = meshes.get(mesh_handle) { - if let Some(aabb) = mesh.compute_aabb() { - commands.entity(entity).try_insert(aabb); - } - } - } -} - -/// Updates [`Frustum`]. -/// -/// This system is used in [`CameraProjectionPlugin`](crate::camera::CameraProjectionPlugin). -pub fn update_frusta( - mut views: Query< - (&GlobalTransform, &Projection, &mut Frustum), - Or<(Changed, Changed)>, - >, -) { - for (transform, projection, mut frustum) in &mut views { - *frustum = projection.compute_frustum(transform); - } -} - -fn visibility_propagate_system( - changed: Query< - (Entity, &Visibility, Option<&ChildOf>, Option<&Children>), - ( - With, - Or<(Changed, Changed)>, - ), - >, - mut visibility_query: Query<(&Visibility, &mut InheritedVisibility)>, - children_query: Query<&Children, (With, With)>, -) { - for (entity, visibility, child_of, children) in &changed { - let is_visible = match visibility { - Visibility::Visible => true, - Visibility::Hidden => false, - // fall back to true if no parent is found or parent lacks components - Visibility::Inherited => child_of - .and_then(|c| visibility_query.get(c.parent()).ok()) - .is_none_or(|(_, x)| x.get()), - }; - let (_, mut inherited_visibility) = visibility_query - .get_mut(entity) - .expect("With ensures this query will return a value"); - - // Only update the visibility if it has changed. - // This will also prevent the visibility from propagating multiple times in the same frame - // if this entity's visibility has been updated recursively by its parent. - if inherited_visibility.get() != is_visible { - inherited_visibility.0 = is_visible; - - // Recursively update the visibility of each child. - for &child in children.into_iter().flatten() { - let _ = - propagate_recursive(is_visible, child, &mut visibility_query, &children_query); - } - } - } -} - -fn propagate_recursive( - parent_is_visible: bool, - entity: Entity, - visibility_query: &mut Query<(&Visibility, &mut InheritedVisibility)>, - children_query: &Query<&Children, (With, With)>, - // BLOCKED: https://github.com/rust-lang/rust/issues/31436 - // We use a result here to use the `?` operator. Ideally we'd use a try block instead -) -> Result<(), ()> { - // Get the visibility components for the current entity. - // If the entity does not have the required components, just return early. - let (visibility, mut inherited_visibility) = visibility_query.get_mut(entity).map_err(drop)?; - - let is_visible = match visibility { - Visibility::Visible => true, - Visibility::Hidden => false, - Visibility::Inherited => parent_is_visible, - }; - - // Only update the visibility if it has changed. - if inherited_visibility.get() != is_visible { - inherited_visibility.0 = is_visible; - - // Recursively update the visibility of each child. - for &child in children_query.get(entity).ok().into_iter().flatten() { - let _ = propagate_recursive(is_visible, child, visibility_query, children_query); - } - } - - Ok(()) -} - -/// Stores all entities that were visible in the previous frame. -/// -/// As systems that check visibility judge entities visible, they remove them -/// from this set. Afterward, the `mark_newly_hidden_entities_invisible` system -/// runs and marks every mesh still remaining in this set as hidden. -#[derive(Resource, Default, Deref, DerefMut)] -pub struct PreviousVisibleEntities(EntityHashSet); - -/// Resets the view visibility of every entity. -/// Entities that are visible will be marked as such later this frame -/// by a [`VisibilitySystems::CheckVisibility`] system. -fn reset_view_visibility( - mut query: Query<(Entity, &ViewVisibility)>, - mut previous_visible_entities: ResMut, -) { - previous_visible_entities.clear(); - - query.iter_mut().for_each(|(entity, view_visibility)| { - // Record the entities that were previously visible. - if view_visibility.get() { - previous_visible_entities.insert(entity); - } - }); -} - -/// System updating the visibility of entities each frame. -/// -/// The system is part of the [`VisibilitySystems::CheckVisibility`] set. Each -/// frame, it updates the [`ViewVisibility`] of all entities, and for each view -/// also compute the [`VisibleEntities`] for that view. -/// -/// To ensure that an entity is checked for visibility, make sure that it has a -/// [`VisibilityClass`] component and that that component is nonempty. -pub fn check_visibility( - mut thread_queues: Local>>>, - mut view_query: Query<( - Entity, - &mut VisibleEntities, - &Frustum, - Option<&RenderLayers>, - &Camera, - Has, - )>, - mut visible_aabb_query: Query<( - Entity, - &InheritedVisibility, - &mut ViewVisibility, - &VisibilityClass, - Option<&RenderLayers>, - Option<&Aabb>, - &GlobalTransform, - Has, - Has, - )>, - visible_entity_ranges: Option>, - mut previous_visible_entities: ResMut, -) { - let visible_entity_ranges = visible_entity_ranges.as_deref(); - - for (view, mut visible_entities, frustum, maybe_view_mask, camera, no_cpu_culling) in - &mut view_query - { - if !camera.is_active { - continue; - } - - let view_mask = maybe_view_mask.unwrap_or_default(); - - visible_aabb_query.par_iter_mut().for_each_init( - || thread_queues.borrow_local_mut(), - |queue, query_item| { - let ( - entity, - inherited_visibility, - mut view_visibility, - visibility_class, - maybe_entity_mask, - maybe_model_aabb, - transform, - no_frustum_culling, - has_visibility_range, - ) = query_item; - - // Skip computing visibility for entities that are configured to be hidden. - // ViewVisibility has already been reset in `reset_view_visibility`. - if !inherited_visibility.get() { - return; - } - - let entity_mask = maybe_entity_mask.unwrap_or_default(); - if !view_mask.intersects(entity_mask) { - return; - } - - // If outside of the visibility range, cull. - if has_visibility_range - && visible_entity_ranges.is_some_and(|visible_entity_ranges| { - !visible_entity_ranges.entity_is_in_range_of_view(entity, view) - }) - { - return; - } - - // If we have an aabb, do frustum culling - if !no_frustum_culling && !no_cpu_culling { - if let Some(model_aabb) = maybe_model_aabb { - let world_from_local = transform.affine(); - let model_sphere = Sphere { - center: world_from_local.transform_point3a(model_aabb.center), - radius: transform.radius_vec3a(model_aabb.half_extents), - }; - // Do quick sphere-based frustum culling - if !frustum.intersects_sphere(&model_sphere, false) { - return; - } - // Do aabb-based frustum culling - if !frustum.intersects_obb(model_aabb, &world_from_local, true, false) { - return; - } - } - } - - // Make sure we don't trigger changed notifications - // unnecessarily by checking whether the flag is set before - // setting it. - if !**view_visibility { - view_visibility.set(); - } - - // Add the entity to the queue for all visibility classes the - // entity is in. - for visibility_class_id in visibility_class.iter() { - queue.entry(*visibility_class_id).or_default().push(entity); - } - }, - ); - - visible_entities.clear_all(); - - // Drain all the thread queues into the `visible_entities` list. - for class_queues in thread_queues.iter_mut() { - for (class, entities) in class_queues { - let visible_entities_for_class = visible_entities.get_mut(*class); - for entity in entities.drain(..) { - // As we mark entities as visible, we remove them from the - // `previous_visible_entities` list. At the end, all of the - // entities remaining in `previous_visible_entities` will be - // entities that were visible last frame but are no longer - // visible this frame. - previous_visible_entities.remove(&entity); - - visible_entities_for_class.push(entity); - } - } - } - } -} - -/// Marks any entities that weren't judged visible this frame as invisible. -/// -/// As visibility-determining systems run, they remove entities that they judge -/// visible from [`PreviousVisibleEntities`]. At the end of visibility -/// determination, all entities that remain in [`PreviousVisibleEntities`] must -/// be invisible. This system goes through those entities and marks them newly -/// invisible (which sets the change flag for them). -fn mark_newly_hidden_entities_invisible( - mut view_visibilities: Query<&mut ViewVisibility>, - mut previous_visible_entities: ResMut, -) { - // Whatever previous visible entities are left are entities that were - // visible last frame but just became invisible. - for entity in previous_visible_entities.drain() { - if let Ok(mut view_visibility) = view_visibilities.get_mut(entity) { - *view_visibility = ViewVisibility::HIDDEN; - } - } -} - -/// A generic component add hook that automatically adds the appropriate -/// [`VisibilityClass`] to an entity. -/// -/// This can be handy when creating custom renderable components. To use this -/// hook, add it to your renderable component like this: -/// -/// ```ignore -/// #[derive(Component)] -/// #[component(on_add = add_visibility_class::)] -/// struct MyComponent { -/// ... -/// } -/// ``` -pub fn add_visibility_class( - mut world: DeferredWorld<'_>, - HookContext { entity, .. }: HookContext, -) where - C: 'static, -{ - if let Some(mut visibility_class) = world.get_mut::(entity) { - visibility_class.push(TypeId::of::()); - } -} - -#[cfg(test)] -mod test { - use super::*; - use bevy_app::prelude::*; - - #[test] - fn visibility_propagation() { - let mut app = App::new(); - app.add_systems(Update, visibility_propagate_system); - - let root1 = app.world_mut().spawn(Visibility::Hidden).id(); - let root1_child1 = app.world_mut().spawn(Visibility::default()).id(); - let root1_child2 = app.world_mut().spawn(Visibility::Hidden).id(); - let root1_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); - let root1_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); - - app.world_mut() - .entity_mut(root1) - .add_children(&[root1_child1, root1_child2]); - app.world_mut() - .entity_mut(root1_child1) - .add_children(&[root1_child1_grandchild1]); - app.world_mut() - .entity_mut(root1_child2) - .add_children(&[root1_child2_grandchild1]); - - let root2 = app.world_mut().spawn(Visibility::default()).id(); - let root2_child1 = app.world_mut().spawn(Visibility::default()).id(); - let root2_child2 = app.world_mut().spawn(Visibility::Hidden).id(); - let root2_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); - let root2_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id(); - - app.world_mut() - .entity_mut(root2) - .add_children(&[root2_child1, root2_child2]); - app.world_mut() - .entity_mut(root2_child1) - .add_children(&[root2_child1_grandchild1]); - app.world_mut() - .entity_mut(root2_child2) - .add_children(&[root2_child2_grandchild1]); - - app.update(); - - let is_visible = |e: Entity| { - app.world() - .entity(e) - .get::() - .unwrap() - .get() - }; - assert!( - !is_visible(root1), - "invisibility propagates down tree from root" - ); - assert!( - !is_visible(root1_child1), - "invisibility propagates down tree from root" - ); - assert!( - !is_visible(root1_child2), - "invisibility propagates down tree from root" - ); - assert!( - !is_visible(root1_child1_grandchild1), - "invisibility propagates down tree from root" - ); - assert!( - !is_visible(root1_child2_grandchild1), - "invisibility propagates down tree from root" - ); - - assert!( - is_visible(root2), - "visibility propagates down tree from root" - ); - assert!( - is_visible(root2_child1), - "visibility propagates down tree from root" - ); - assert!( - !is_visible(root2_child2), - "visibility propagates down tree from root, but local invisibility is preserved" - ); - assert!( - is_visible(root2_child1_grandchild1), - "visibility propagates down tree from root" - ); - assert!( - !is_visible(root2_child2_grandchild1), - "child's invisibility propagates down to grandchild" - ); - } - - #[test] - fn test_visibility_propagation_on_parent_change() { - // Setup the world and schedule - let mut app = App::new(); - - app.add_systems(Update, visibility_propagate_system); - - // Create entities with visibility and hierarchy - let parent1 = app.world_mut().spawn((Visibility::Hidden,)).id(); - let parent2 = app.world_mut().spawn((Visibility::Visible,)).id(); - let child1 = app.world_mut().spawn((Visibility::Inherited,)).id(); - let child2 = app.world_mut().spawn((Visibility::Inherited,)).id(); - - // Build hierarchy - app.world_mut() - .entity_mut(parent1) - .add_children(&[child1, child2]); - - // Run the system initially to set up visibility - app.update(); - - // Change parent visibility to Hidden - app.world_mut() - .entity_mut(parent2) - .insert(Visibility::Visible); - // Simulate a change in the parent component - app.world_mut().entity_mut(child2).insert(ChildOf(parent2)); // example of changing parent - - // Run the system again to propagate changes - app.update(); - - let is_visible = |e: Entity| { - app.world() - .entity(e) - .get::() - .unwrap() - .get() - }; - - // Retrieve and assert visibility - - assert!( - !is_visible(child1), - "Child1 should inherit visibility from parent" - ); - - assert!( - is_visible(child2), - "Child2 should inherit visibility from parent" - ); - } - - #[test] - fn visibility_propagation_unconditional_visible() { - use Visibility::{Hidden, Inherited, Visible}; - - let mut app = App::new(); - app.add_systems(Update, visibility_propagate_system); - - let root1 = app.world_mut().spawn(Visible).id(); - let root1_child1 = app.world_mut().spawn(Inherited).id(); - let root1_child2 = app.world_mut().spawn(Hidden).id(); - let root1_child1_grandchild1 = app.world_mut().spawn(Visible).id(); - let root1_child2_grandchild1 = app.world_mut().spawn(Visible).id(); - - let root2 = app.world_mut().spawn(Inherited).id(); - let root3 = app.world_mut().spawn(Hidden).id(); - - app.world_mut() - .entity_mut(root1) - .add_children(&[root1_child1, root1_child2]); - app.world_mut() - .entity_mut(root1_child1) - .add_children(&[root1_child1_grandchild1]); - app.world_mut() - .entity_mut(root1_child2) - .add_children(&[root1_child2_grandchild1]); - - app.update(); - - let is_visible = |e: Entity| { - app.world() - .entity(e) - .get::() - .unwrap() - .get() - }; - assert!( - is_visible(root1), - "an unconditionally visible root is visible" - ); - assert!( - is_visible(root1_child1), - "an inheriting child of an unconditionally visible parent is visible" - ); - assert!( - !is_visible(root1_child2), - "a hidden child on an unconditionally visible parent is hidden" - ); - assert!( - is_visible(root1_child1_grandchild1), - "an unconditionally visible child of an inheriting parent is visible" - ); - assert!( - is_visible(root1_child2_grandchild1), - "an unconditionally visible child of a hidden parent is visible" - ); - assert!(is_visible(root2), "an inheriting root is visible"); - assert!(!is_visible(root3), "a hidden root is hidden"); - } - - #[test] - fn visibility_propagation_change_detection() { - let mut world = World::new(); - let mut schedule = Schedule::default(); - schedule.add_systems(visibility_propagate_system); - - // Set up an entity hierarchy. - - let id1 = world.spawn(Visibility::default()).id(); - - let id2 = world.spawn(Visibility::default()).id(); - world.entity_mut(id1).add_children(&[id2]); - - let id3 = world.spawn(Visibility::Hidden).id(); - world.entity_mut(id2).add_children(&[id3]); - - let id4 = world.spawn(Visibility::default()).id(); - world.entity_mut(id3).add_children(&[id4]); - - // Test the hierarchy. - - // Make sure the hierarchy is up-to-date. - schedule.run(&mut world); - world.clear_trackers(); - - let mut q = world.query::>(); - - assert!(!q.get(&world, id1).unwrap().is_changed()); - assert!(!q.get(&world, id2).unwrap().is_changed()); - assert!(!q.get(&world, id3).unwrap().is_changed()); - assert!(!q.get(&world, id4).unwrap().is_changed()); - - world.clear_trackers(); - world.entity_mut(id1).insert(Visibility::Hidden); - schedule.run(&mut world); - - assert!(q.get(&world, id1).unwrap().is_changed()); - assert!(q.get(&world, id2).unwrap().is_changed()); - assert!(!q.get(&world, id3).unwrap().is_changed()); - assert!(!q.get(&world, id4).unwrap().is_changed()); - - world.clear_trackers(); - schedule.run(&mut world); - - assert!(!q.get(&world, id1).unwrap().is_changed()); - assert!(!q.get(&world, id2).unwrap().is_changed()); - assert!(!q.get(&world, id3).unwrap().is_changed()); - assert!(!q.get(&world, id4).unwrap().is_changed()); - - world.clear_trackers(); - world.entity_mut(id3).insert(Visibility::Inherited); - schedule.run(&mut world); - - assert!(!q.get(&world, id1).unwrap().is_changed()); - assert!(!q.get(&world, id2).unwrap().is_changed()); - assert!(!q.get(&world, id3).unwrap().is_changed()); - assert!(!q.get(&world, id4).unwrap().is_changed()); - - world.clear_trackers(); - world.entity_mut(id2).insert(Visibility::Visible); - schedule.run(&mut world); - - assert!(!q.get(&world, id1).unwrap().is_changed()); - assert!(q.get(&world, id2).unwrap().is_changed()); - assert!(q.get(&world, id3).unwrap().is_changed()); - assert!(q.get(&world, id4).unwrap().is_changed()); - - world.clear_trackers(); - schedule.run(&mut world); - - assert!(!q.get(&world, id1).unwrap().is_changed()); - assert!(!q.get(&world, id2).unwrap().is_changed()); - assert!(!q.get(&world, id3).unwrap().is_changed()); - assert!(!q.get(&world, id4).unwrap().is_changed()); - } - - #[test] - fn visibility_propagation_with_invalid_parent() { - let mut world = World::new(); - let mut schedule = Schedule::default(); - schedule.add_systems(visibility_propagate_system); - - let parent = world.spawn(()).id(); - let child = world.spawn(Visibility::default()).id(); - world.entity_mut(parent).add_children(&[child]); - - schedule.run(&mut world); - world.clear_trackers(); - - let child_visible = world.entity(child).get::().unwrap().0; - // defaults to same behavior of parent not found: visible = true - assert!(child_visible); - } - - #[test] - fn ensure_visibility_enum_size() { - assert_eq!(1, size_of::()); - assert_eq!(1, size_of::>()); - } -} diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 2559d3b8d2..543f10f564 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -1,37 +1,26 @@ //! Specific distances from the camera in which entities are visible, also known //! as *hierarchical levels of detail* or *HLOD*s. -use core::{ - hash::{Hash, Hasher}, - ops::Range, -}; - -use bevy_app::{App, Plugin, PostUpdate}; +use super::VisibilityRange; +use bevy_app::{App, Plugin}; use bevy_ecs::{ - component::Component, - entity::{Entity, EntityHashMap}, - query::{Changed, With}, - reflect::ReflectComponent, - removal_detection::RemovedComponents, + entity::Entity, + lifecycle::RemovedComponents, + query::Changed, resource::Resource, schedule::IntoScheduleConfigs as _, - system::{Local, Query, Res, ResMut}, + system::{Query, Res, ResMut}, }; -use bevy_math::{vec4, FloatOrd, Vec4}; +use bevy_math::{vec4, Vec4}; use bevy_platform::collections::HashMap; -use bevy_reflect::Reflect; -use bevy_transform::components::GlobalTransform; -use bevy_utils::{prelude::default, Parallel}; +use bevy_utils::prelude::default; use nonmax::NonMaxU16; use wgpu::{BufferBindingType, BufferUsages}; -use super::{check_visibility, VisibilitySystems}; -use crate::sync_world::{MainEntity, MainEntityHashMap}; use crate::{ - camera::Camera, - primitives::Aabb, render_resource::BufferVec, renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, MainEntityHashMap}, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; @@ -48,21 +37,12 @@ pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4; /// buffer instead (most notably, on WebGL 2). const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: usize = 64; -/// A plugin that enables [`VisibilityRange`]s, which allow entities to be +/// A plugin that enables [`RenderVisibilityRanges`]s, which allow entities to be /// hidden or shown based on distance to the camera. -pub struct VisibilityRangePlugin; +pub struct RenderVisibilityRangePlugin; -impl Plugin for VisibilityRangePlugin { +impl Plugin for RenderVisibilityRangePlugin { fn build(&self, app: &mut App) { - app.register_type::() - .init_resource::() - .add_systems( - PostUpdate, - check_visibility_ranges - .in_set(VisibilitySystems::CheckVisibility) - .before(check_visibility), - ); - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -77,137 +57,6 @@ impl Plugin for VisibilityRangePlugin { } } -/// Specifies the range of distances that this entity must be from the camera in -/// order to be rendered. -/// -/// This is also known as *hierarchical level of detail* or *HLOD*. -/// -/// Use this component when you want to render a high-polygon mesh when the -/// camera is close and a lower-polygon mesh when the camera is far away. This -/// is a common technique for improving performance, because fine details are -/// hard to see in a mesh at a distance. To avoid an artifact known as *popping* -/// between levels, each level has a *margin*, within which the object -/// transitions gradually from invisible to visible using a dithering effect. -/// -/// You can also use this feature to replace multiple meshes with a single mesh -/// when the camera is distant. This is the reason for the term "*hierarchical* -/// level of detail". Reducing the number of meshes can be useful for reducing -/// drawcall count. Note that you must place the [`VisibilityRange`] component -/// on each entity you want to be part of a LOD group, as [`VisibilityRange`] -/// isn't automatically propagated down to children. -/// -/// A typical use of this feature might look like this: -/// -/// | Entity | `start_margin` | `end_margin` | -/// |-------------------------|----------------|--------------| -/// | Root | N/A | N/A | -/// | ├─ High-poly mesh | [0, 0) | [20, 25) | -/// | ├─ Low-poly mesh | [20, 25) | [70, 75) | -/// | └─ Billboard *imposter* | [70, 75) | [150, 160) | -/// -/// With this setup, the user will see a high-poly mesh when the camera is -/// closer than 20 units. As the camera zooms out, between 20 units to 25 units, -/// the high-poly mesh will gradually fade to a low-poly mesh. When the camera -/// is 70 to 75 units away, the low-poly mesh will fade to a single textured -/// quad. And between 150 and 160 units, the object fades away entirely. Note -/// that the `end_margin` of a higher LOD is always identical to the -/// `start_margin` of the next lower LOD; this is important for the crossfade -/// effect to function properly. -#[derive(Component, Clone, PartialEq, Default, Reflect)] -#[reflect(Component, PartialEq, Hash, Clone)] -pub struct VisibilityRange { - /// The range of distances, in world units, between which this entity will - /// smoothly fade into view as the camera zooms out. - /// - /// If the start and end of this range are identical, the transition will be - /// abrupt, with no crossfading. - /// - /// `start_margin.end` must be less than or equal to `end_margin.start`. - pub start_margin: Range, - - /// The range of distances, in world units, between which this entity will - /// smoothly fade out of view as the camera zooms out. - /// - /// If the start and end of this range are identical, the transition will be - /// abrupt, with no crossfading. - /// - /// `end_margin.start` must be greater than or equal to `start_margin.end`. - pub end_margin: Range, - - /// If set to true, Bevy will use the center of the axis-aligned bounding - /// box ([`Aabb`]) as the position of the mesh for the purposes of - /// visibility range computation. - /// - /// Otherwise, if this field is set to false, Bevy will use the origin of - /// the mesh as the mesh's position. - /// - /// Usually you will want to leave this set to false, because different LODs - /// may have different AABBs, and smooth crossfades between LOD levels - /// require that all LODs of a mesh be at *precisely* the same position. If - /// you aren't using crossfading, however, and your meshes aren't centered - /// around their origins, then this flag may be useful. - pub use_aabb: bool, -} - -impl Eq for VisibilityRange {} - -impl Hash for VisibilityRange { - fn hash(&self, state: &mut H) - where - H: Hasher, - { - FloatOrd(self.start_margin.start).hash(state); - FloatOrd(self.start_margin.end).hash(state); - FloatOrd(self.end_margin.start).hash(state); - FloatOrd(self.end_margin.end).hash(state); - } -} - -impl VisibilityRange { - /// Creates a new *abrupt* visibility range, with no crossfade. - /// - /// There will be no crossfade; the object will immediately vanish if the - /// camera is closer than `start` units or farther than `end` units from the - /// model. - /// - /// The `start` value must be less than or equal to the `end` value. - #[inline] - pub fn abrupt(start: f32, end: f32) -> Self { - Self { - start_margin: start..start, - end_margin: end..end, - use_aabb: false, - } - } - - /// Returns true if both the start and end transitions for this range are - /// abrupt: that is, there is no crossfading. - #[inline] - pub fn is_abrupt(&self) -> bool { - self.start_margin.start == self.start_margin.end - && self.end_margin.start == self.end_margin.end - } - - /// Returns true if the object will be visible at all, given a camera - /// `camera_distance` units away. - /// - /// Any amount of visibility, even with the heaviest dithering applied, is - /// considered visible according to this check. - #[inline] - pub fn is_visible_at_all(&self, camera_distance: f32) -> bool { - camera_distance >= self.start_margin.start && camera_distance < self.end_margin.end - } - - /// Returns true if the object is completely invisible, given a camera - /// `camera_distance` units away. - /// - /// This is equivalent to `!VisibilityRange::is_visible_at_all()`. - #[inline] - pub fn is_culled(&self, camera_distance: f32) -> bool { - !self.is_visible_at_all(camera_distance) - } -} - /// Stores information related to [`VisibilityRange`]s in the render world. #[derive(Resource)] pub struct RenderVisibilityRanges { @@ -313,128 +162,6 @@ impl RenderVisibilityRanges { } } -/// Stores which entities are in within the [`VisibilityRange`]s of views. -/// -/// This doesn't store the results of frustum or occlusion culling; use -/// [`super::ViewVisibility`] for that. Thus entities in this list may not -/// actually be visible. -/// -/// For efficiency, these tables only store entities that have -/// [`VisibilityRange`] components. Entities without such a component won't be -/// in these tables at all. -/// -/// The table is indexed by entity and stores a 32-bit bitmask with one bit for -/// each camera, where a 0 bit corresponds to "out of range" and a 1 bit -/// corresponds to "in range". Hence it's limited to storing information for 32 -/// views. -#[derive(Resource, Default)] -pub struct VisibleEntityRanges { - /// Stores which bit index each view corresponds to. - views: EntityHashMap, - - /// Stores a bitmask in which each view has a single bit. - /// - /// A 0 bit for a view corresponds to "out of range"; a 1 bit corresponds to - /// "in range". - entities: EntityHashMap, -} - -impl VisibleEntityRanges { - /// Clears out the [`VisibleEntityRanges`] in preparation for a new frame. - fn clear(&mut self) { - self.views.clear(); - self.entities.clear(); - } - - /// Returns true if the entity is in range of the given camera. - /// - /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or - /// occlusion culling. Thus the entity might not *actually* be visible. - /// - /// The entity is assumed to have a [`VisibilityRange`] component. If the - /// entity doesn't have that component, this method will return false. - #[inline] - pub fn entity_is_in_range_of_view(&self, entity: Entity, view: Entity) -> bool { - let Some(visibility_bitmask) = self.entities.get(&entity) else { - return false; - }; - let Some(view_index) = self.views.get(&view) else { - return false; - }; - (visibility_bitmask & (1 << view_index)) != 0 - } - - /// Returns true if the entity is in range of any view. - /// - /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or - /// occlusion culling. Thus the entity might not *actually* be visible. - /// - /// The entity is assumed to have a [`VisibilityRange`] component. If the - /// entity doesn't have that component, this method will return false. - #[inline] - pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool { - self.entities.contains_key(&entity) - } -} - -/// Checks all entities against all views in order to determine which entities -/// with [`VisibilityRange`]s are potentially visible. -/// -/// This only checks distance from the camera and doesn't frustum or occlusion -/// cull. -pub fn check_visibility_ranges( - mut visible_entity_ranges: ResMut, - view_query: Query<(Entity, &GlobalTransform), With>, - mut par_local: Local>>, - entity_query: Query<(Entity, &GlobalTransform, Option<&Aabb>, &VisibilityRange)>, -) { - visible_entity_ranges.clear(); - - // Early out if the visibility range feature isn't in use. - if entity_query.is_empty() { - return; - } - - // Assign an index to each view. - let mut views = vec![]; - for (view, view_transform) in view_query.iter().take(32) { - let view_index = views.len() as u8; - visible_entity_ranges.views.insert(view, view_index); - views.push((view, view_transform.translation_vec3a())); - } - - // Check each entity/view pair. Only consider entities with - // [`VisibilityRange`] components. - entity_query.par_iter().for_each( - |(entity, entity_transform, maybe_model_aabb, visibility_range)| { - let mut visibility = 0; - for (view_index, &(_, view_position)) in views.iter().enumerate() { - // If instructed to use the AABB and the model has one, use its - // center as the model position. Otherwise, use the model's - // translation. - let model_position = match (visibility_range.use_aabb, maybe_model_aabb) { - (true, Some(model_aabb)) => entity_transform - .affine() - .transform_point3a(model_aabb.center), - _ => entity_transform.translation_vec3a(), - }; - - if visibility_range.is_visible_at_all((view_position - model_position).length()) { - visibility |= 1 << view_index; - } - } - - // Invisible entities have no entry at all in the hash map. This speeds - // up checks slightly in this common case. - if visibility != 0 { - par_local.borrow_local_mut().push((entity, visibility)); - } - }, - ); - - visible_entity_ranges.entities.extend(par_local.drain()); -} - /// Extracts all [`VisibilityRange`] components from the main world to the /// render world and inserts them into [`RenderVisibilityRanges`]. pub fn extract_visibility_ranges( diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 4c8d86d040..657106d5a0 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -1,12 +1,13 @@ use crate::{ render_resource::{SurfaceTexture, TextureView}, renderer::{RenderAdapter, RenderDevice, RenderInstance}, - Extract, ExtractSchedule, Render, RenderApp, RenderSystems, WgpuWrapper, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashMap, prelude::*}; use bevy_platform::collections::HashSet; use bevy_utils::default; +use bevy_utils::WgpuWrapper; use bevy_window::{ CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, }; diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 854a6bc064..4e74ea5924 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -1,6 +1,6 @@ use super::ExtractedWindows; use crate::{ - camera::{ManualTextureViewHandle, ManualTextureViews, NormalizedRenderTarget, RenderTarget}, + camera::{NormalizedRenderTarget, ToNormalizedRenderTarget as _}, gpu_readback, prelude::Shader, render_asset::{RenderAssetUsages, RenderAssets}, @@ -11,18 +11,19 @@ use crate::{ SpecializedRenderPipelines, Texture, TextureUsages, TextureView, VertexState, }, renderer::RenderDevice, - texture::{GpuImage, OutputColorAttachment}, + texture::{GpuImage, ManualTextureViews, OutputColorAttachment}, view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces}, ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems, }; use alloc::{borrow::Cow, sync::Arc}; use bevy_app::{First, Plugin, Update}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use bevy_camera::{ManualTextureViewHandle, RenderTarget}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ entity::EntityHashMap, event::event_update_system, prelude::*, system::SystemState, }; -use bevy_image::{Image, TextureFormatPixelInfo}; +use bevy_image::{Image, TextureFormatPixelInfo, ToExtents}; use bevy_platform::collections::HashSet; use bevy_reflect::Reflect; use bevy_tasks::AsyncComputeTaskPool; @@ -39,7 +40,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 +123,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(); @@ -321,11 +322,7 @@ fn prepare_screenshots( continue; }; let format = manual_texture_view.format; - let size = Extent3d { - width: manual_texture_view.size.x, - height: manual_texture_view.size.y, - ..default() - }; + let size = manual_texture_view.size.to_extents(); let (texture_view, state) = prepare_screenshot_state( size, format, @@ -392,9 +389,6 @@ fn prepare_screenshot_state( pub struct ScreenshotPlugin; -const SCREENSHOT_SHADER_HANDLE: Handle = - weak_handle!("c31753d6-326a-47cb-a359-65c97a471fda"); - impl Plugin for ScreenshotPlugin { fn build(&self, app: &mut bevy_app::App) { app.add_systems( @@ -403,21 +397,16 @@ impl Plugin for ScreenshotPlugin { .after(event_update_system) .before(ApplyDeferred), ) - .add_systems(Update, trigger_screenshots) .register_type::() .register_type::(); - load_internal_asset!( - app, - SCREENSHOT_SHADER_HANDLE, - "screenshot.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "screenshot.wgsl"); } fn finish(&self, app: &mut bevy_app::App) { let (tx, rx) = std::sync::mpsc::channel(); - app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))); + app.add_systems(Update, trigger_screenshots) + .insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -441,6 +430,7 @@ impl Plugin for ScreenshotPlugin { #[derive(Resource)] pub struct ScreenshotToScreenPipeline { pub bind_group_layout: BindGroupLayout, + pub shader: Handle, } impl FromWorld for ScreenshotToScreenPipeline { @@ -455,7 +445,12 @@ impl FromWorld for ScreenshotToScreenPipeline { ), ); - Self { bind_group_layout } + let shader = load_embedded_asset!(render_world, "screenshot.wgsl"); + + Self { + bind_group_layout, + shader, + } } } @@ -467,29 +462,24 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { label: Some(Cow::Borrowed("screenshot-to-screen")), layout: vec![self.bind_group_layout.clone()], vertex: VertexState { - buffers: vec![], - shader_defs: vec![], - entry_point: Cow::Borrowed("vs_main"), - shader: SCREENSHOT_SHADER_HANDLE, + shader: self.shader.clone(), + ..default() }, primitive: wgpu::PrimitiveState { cull_mode: Some(wgpu::Face::Back), ..Default::default() }, - depth_stencil: None, multisample: Default::default(), fragment: Some(FragmentState { - shader: SCREENSHOT_SHADER_HANDLE, - entry_point: Cow::Borrowed("fs_main"), - shader_defs: vec![], + shader: self.shader.clone(), targets: vec![Some(wgpu::ColorTargetState { format: key, blend: None, write_mask: wgpu::ColorWrites::ALL, })], + ..default() }), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 3bb913c859..48d718b410 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index c9e594107e..ee0b15847a 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -350,7 +350,7 @@ impl<'w> DynamicSceneBuilder<'w> { let original_world_dqf_id = self .original_world .components() - .get_resource_id(TypeId::of::()); + .get_valid_resource_id(TypeId::of::()); let type_registry = self.original_world.resource::().read(); diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index a507a58aaf..9b0845f80f 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Provides scene definition, instantiation and serialization/deserialization. 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 3bf8ca9f64..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. @@ -79,6 +80,7 @@ pub struct SceneSpawner { scenes_to_despawn: Vec>, instances_to_despawn: Vec, scenes_with_parent: Vec<(InstanceId, Entity)>, + instances_ready: Vec<(InstanceId, Option)>, } /// Errors that can occur when spawning a scene. @@ -104,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( @@ -337,8 +339,9 @@ impl SceneSpawner { // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - // Defer via commands otherwise SceneSpawner is not available in the observer. - world.commands().trigger(SceneInstanceReady { instance_id }); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); } } Err(SceneSpawnError::NonExistentScene { .. }) => { @@ -362,8 +365,9 @@ impl SceneSpawner { // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - // Defer via commands otherwise SceneSpawner is not available in the observer. - world.commands().trigger(SceneInstanceReady { instance_id }); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); } } Err(SceneSpawnError::NonExistentRealScene { .. }) => { @@ -398,12 +402,25 @@ impl SceneSpawner { } } + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, Some(parent))); + } else { + self.scenes_with_parent.push((instance_id, parent)); + } + } + } + + fn trigger_scene_ready_events(&mut self, world: &mut World) { + for (instance_id, parent) in self.instances_ready.drain(..) { + if let Some(parent) = parent { // Defer via commands otherwise SceneSpawner is not available in the observer. world .commands() .trigger_targets(SceneInstanceReady { instance_id }, parent); } else { - self.scenes_with_parent.push((instance_id, parent)); + // Defer via commands otherwise SceneSpawner is not available in the observer. + world.commands().trigger(SceneInstanceReady { instance_id }); } } } @@ -477,6 +494,7 @@ pub fn scene_spawner_system(world: &mut World) { .update_spawned_scenes(world, &updated_spawned_scenes) .unwrap(); scene_spawner.set_scene_instance_parent_sync(world); + scene_spawner.trigger_scene_ready_events(world); }); } @@ -525,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}, @@ -704,10 +722,10 @@ mod tests { .expect("Failed to run dynamic scene builder system.") } - fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Entity) { + 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!( @@ -717,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!( @@ -756,7 +774,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -775,7 +793,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -799,7 +817,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] @@ -823,7 +841,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] 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..40eaab2aab --- /dev/null +++ b/crates/bevy_solari/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "bevy_solari" +version = "0.17.0-dev" +edition = "2024" +description = "Provides raytraced lighting for Bevy Engine" +homepage = "https://bevy.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_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", 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..30cc15ba10 --- /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::{RenderGraphExt, 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..325ea42dac --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/node.rs @@ -0,0 +1,132 @@ +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}, +}; +use bevy_utils::default; + +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(), + ], + shader: load_embedded_asset!(world, "pathtracer.wgsl"), + ..default() + }); + + 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..ddef965222 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/prepare.rs @@ -0,0 +1,47 @@ +use super::Pathtracer; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::ToExtents; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{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: viewport.to_extents(), + 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..a8d6235f30 --- /dev/null +++ b/crates/bevy_solari/src/realtime/mod.rs @@ -0,0 +1,89 @@ +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::{ + render_graph::{RenderGraphExt, 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"); + + 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..2fcc29b415 --- /dev/null +++ b/crates/bevy_solari/src/realtime/node.rs @@ -0,0 +1,245 @@ +use super::{prepare::SolariLightingResources, SolariLighting}; +use crate::scene::RaytracingSceneBindings; +use bevy_asset::load_embedded_asset; +use bevy_core_pipeline::prepass::{ + PreviousViewData, PreviousViewUniformOffset, PreviousViewUniforms, ViewPrepassTextures, +}; +use bevy_diagnostic::FrameCount; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_image::ToExtents; +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}, +}; +use bevy_utils::default; + +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, + &'static PreviousViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + solari_lighting, + solari_lighting_resources, + camera, + view_target, + view_prepass_textures, + view_uniform_offset, + previous_view_uniform_offset, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let previous_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), + Some(previous_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(), + previous_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, + &solari_lighting_resources.previous_gbuffer.1, + &solari_lighting_resources.previous_depth.1, + view_uniforms, + previous_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, + previous_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); + drop(pass); + + // TODO: Remove these copies, and double buffer instead + command_encoder.copy_texture_to_texture( + view_prepass_textures + .deferred + .clone() + .unwrap() + .texture + .texture + .as_image_copy(), + solari_lighting_resources.previous_gbuffer.0.as_image_copy(), + viewport.to_extents(), + ); + command_encoder.copy_texture_to_texture( + view_prepass_textures + .depth + .clone() + .unwrap() + .texture + .texture + .as_image_copy(), + solari_lighting_resources.previous_depth.0.as_image_copy(), + viewport.to_extents(), + ); + + 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 }), + texture_2d(TextureSampleType::Uint), + texture_depth_2d(), + uniform_buffer::(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"), + entry_point: Some("initial_and_temporal".into()), + ..default() + }); + + 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"), + entry_point: Some("spatial_and_shade".into()), + ..default() + }); + + 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..992a75c451 --- /dev/null +++ b/crates/bevy_solari/src/realtime/prepare.rs @@ -0,0 +1,98 @@ +use super::SolariLighting; +use bevy_core_pipeline::{core_3d::CORE_3D_DEPTH_FORMAT, deferred::DEFERRED_PREPASS_FORMAT}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res}, +}; +use bevy_image::ToExtents; +use bevy_math::UVec2; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, Texture, TextureDescriptor, TextureDimension, + TextureUsages, TextureView, TextureViewDescriptor, + }, + 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 previous_gbuffer: (Texture, TextureView), + pub previous_depth: (Texture, TextureView), + 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, + }); + + let previous_gbuffer = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_previous_gbuffer"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_PREPASS_FORMAT, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); + let previous_gbuffer_view = previous_gbuffer.create_view(&TextureViewDescriptor::default()); + + let previous_depth = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_previous_depth"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); + let previous_depth_view = previous_depth.create_view(&TextureViewDescriptor::default()); + + commands.entity(entity).insert(SolariLightingResources { + reservoirs_a, + reservoirs_b, + previous_gbuffer: (previous_gbuffer, previous_gbuffer_view), + previous_depth: (previous_depth, previous_depth_view), + view_size, + }); + } +} 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..70de4564cc --- /dev/null +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -0,0 +1,289 @@ +// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf + +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::prepass_bindings::PreviousViewUniforms +#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::sampling::{LightSample, generate_random_light_sample, calculate_light_contribution, trace_light_visibility, sample_disk} +#import bevy_solari::scene_bindings::{previous_frame_light_id_translations, LIGHT_NOT_PRESENT_THIS_FRAME} + +@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 previous_gbuffer: texture_2d; +@group(1) @binding(7) var previous_depth_buffer: texture_depth_2d; +@group(1) @binding(8) var view: View; +@group(1) @binding(9) var previous_view: PreviousViewUniforms; +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; + +const NULL_RESERVOIR_SAMPLE = 0xFFFFFFFFu; + +@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); + let temporal_reservoir = load_temporal_reservoir(global_id.xy, depth, world_position, world_normal); + let combined_reservoir = merge_reservoirs(initial_reservoir, temporal_reservoir, world_position, world_normal, diffuse_brdf, &rng); + + reservoirs_b[pixel_index] = combined_reservoir.merged_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]; + let spatial_reservoir = load_spatial_reservoir(global_id.xy, depth, world_position, world_normal, &rng); + let merge_result = merge_reservoirs(input_reservoir, spatial_reservoir, world_position, world_normal, diffuse_brdf, &rng); + let combined_reservoir = merge_result.merged_reservoir; + + reservoirs_a[pixel_index] = combined_reservoir; + + var pixel_color = merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight * combined_reservoir.visibility; + 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.visibility = trace_light_visibility(reservoir.sample, world_position); + reservoir.unbiased_contribution_weight *= reservoir.visibility; + } + + reservoir.confidence_weight = 1.0; + return reservoir; +} + +fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> Reservoir { + let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; + let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.viewport.zw)); + let temporal_pixel_id = vec2(temporal_pixel_id_float); + if any(temporal_pixel_id_float < vec2(0.0)) || any(temporal_pixel_id_float >= view.viewport.zw) || bool(constants.reset) { + return empty_reservoir(); + } + + let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); + let temporal_gpixel = textureLoad(previous_gbuffer, temporal_pixel_id, 0); + let temporal_world_position = reconstruct_previous_world_position(temporal_pixel_id, temporal_depth); + let temporal_world_normal = octahedral_decode(unpack_24bit_normal(temporal_gpixel.a)); + if pixel_dissimilar(depth, world_position, temporal_world_position, world_normal, temporal_world_normal) { + return empty_reservoir(); + } + + let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.viewport.z); + var temporal_reservoir = reservoirs_a[temporal_pixel_index]; + + temporal_reservoir.sample.light_id.x = previous_frame_light_id_translations[temporal_reservoir.sample.light_id.x]; + if temporal_reservoir.sample.light_id.x == LIGHT_NOT_PRESENT_THIS_FRAME { + return empty_reservoir(); + } + + temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP); + + return temporal_reservoir; +} + +fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { + let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); + + let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); + let spatial_gpixel = textureLoad(gbuffer, spatial_pixel_id, 0); + let spatial_world_position = reconstruct_world_position(spatial_pixel_id, spatial_depth); + let spatial_world_normal = octahedral_decode(unpack_24bit_normal(spatial_gpixel.a)); + if pixel_dissimilar(depth, world_position, spatial_world_position, world_normal, spatial_world_normal) { + return empty_reservoir(); + } + + let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.viewport.z); + var spatial_reservoir = reservoirs_b[spatial_pixel_index]; + + if reservoir_valid(spatial_reservoir) { + spatial_reservoir.visibility = trace_light_visibility(spatial_reservoir.sample, world_position); + } + + return spatial_reservoir; +} + +fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { + var spatial_id = vec2(center_pixel_id) + vec2(sample_disk(SPATIAL_REUSE_RADIUS_PIXELS, rng)); + spatial_id = clamp(spatial_id, vec2(0i), vec2(view.viewport.zw) - 1i); + return vec2(spatial_id); +} + +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; +} + +fn reconstruct_previous_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 = previous_view.world_from_clip * vec4(xy_ndc, depth, 1.0); + return world_pos.xyz / world_pos.w; +} + +// Reject if tangent plane difference difference more than 0.3% or angle between normals more than 25 degrees +fn pixel_dissimilar(depth: f32, world_position: vec3, other_world_position: vec3, normal: vec3, other_normal: vec3) -> bool { + // https://developer.download.nvidia.com/video/gputechconf/gtc/2020/presentations/s22699-fast-denoising-with-self-stabilizing-recurrent-blurs.pdf#page=45 + let tangent_plane_distance = abs(dot(normal, other_world_position - world_position)); + let view_z = -depth_ndc_to_view_z(depth); + + return tangent_plane_distance / view_z > 0.003 || dot(normal, other_normal) < 0.906; +} + +fn depth_ndc_to_view_z(ndc_depth: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -view.clip_from_view[3][2]() / ndc_depth; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return -(view.clip_from_view[3][2] - ndc_depth) / view.clip_from_view[2][2]; +#else + let view_pos = view.view_from_clip * vec4(0.0, 0.0, ndc_depth, 1.0); + return view_pos.z / view_pos.w; +#endif +} + +// 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, + visibility: 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; +} + +struct ReservoirMergeResult { + merged_reservoir: Reservoir, + selected_sample_radiance: vec3, +} + +fn merge_reservoirs( + canonical_reservoir: Reservoir, + other_reservoir: Reservoir, + world_position: vec3, + world_normal: vec3, + diffuse_brdf: vec3, + rng: ptr, +) -> ReservoirMergeResult { + // TODO: Balance heuristic MIS weights + let mis_weight_denominator = 1.0 / (canonical_reservoir.confidence_weight + other_reservoir.confidence_weight); + + let canonical_mis_weight = canonical_reservoir.confidence_weight * mis_weight_denominator; + let canonical_target_function = reservoir_target_function(canonical_reservoir, world_position, world_normal, diffuse_brdf); + let canonical_resampling_weight = canonical_mis_weight * (canonical_target_function.a * canonical_reservoir.unbiased_contribution_weight); + + let other_mis_weight = other_reservoir.confidence_weight * mis_weight_denominator; + let other_target_function = reservoir_target_function(other_reservoir, world_position, world_normal, diffuse_brdf); + let other_resampling_weight = other_mis_weight * (other_target_function.a * other_reservoir.unbiased_contribution_weight); + + var combined_reservoir = empty_reservoir(); + combined_reservoir.weight_sum = canonical_resampling_weight + other_resampling_weight; + combined_reservoir.confidence_weight = canonical_reservoir.confidence_weight + other_reservoir.confidence_weight; + + // https://yusuketokuyoshi.com/papers/2024/Efficient_Visibility_Reuse_for_Real-time_ReSTIR_(Supplementary_Document).pdf + combined_reservoir.visibility = max(0.0, (canonical_reservoir.visibility * canonical_resampling_weight + + other_reservoir.visibility * other_resampling_weight) / combined_reservoir.weight_sum); + + if rand_f(rng) < other_resampling_weight / combined_reservoir.weight_sum { + combined_reservoir.sample = other_reservoir.sample; + + let inverse_target_function = select(0.0, 1.0 / other_target_function.a, other_target_function.a > 0.0); + combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; + + return ReservoirMergeResult(combined_reservoir, other_target_function.rgb); + } else { + combined_reservoir.sample = canonical_reservoir.sample; + + let inverse_target_function = select(0.0, 1.0 / canonical_target_function.a, canonical_target_function.a > 0.0); + combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; + + return ReservoirMergeResult(combined_reservoir, canonical_target_function.rgb); + } +} + +fn reservoir_target_function(reservoir: Reservoir, world_position: vec3, world_normal: vec3, diffuse_brdf: vec3) -> vec4 { + if !reservoir_valid(reservoir) { return vec4(0.0); } + let light_contribution = calculate_light_contribution(reservoir.sample, world_position, world_normal).radiance; + let target_function = luminance(light_contribution * diffuse_brdf); + return vec4(light_contribution, target_function); +} diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs new file mode 100644 index 0000000000..f14b5dbe23 --- /dev/null +++ b/crates/bevy_solari/src/scene/binder.rs @@ -0,0 +1,403 @@ +use super::{blas::BlasManager, extract::StandardMaterialAssets, RaytracingMesh3d}; +use bevy_asset::{AssetId, Handle}; +use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_ecs::{ + entity::{Entity, EntityHashMap}, + 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; + +const TEXTURE_MAP_NONE: u32 = u32::MAX; +const LIGHT_NOT_PRESENT_THIS_FRAME: u32 = u32::MAX; + +#[derive(Resource)] +pub struct RaytracingSceneBindings { + pub bind_group: Option, + pub bind_group_layout: BindGroupLayout, + previous_frame_light_entities: Vec, +} + +pub fn prepare_raytracing_scene_bindings( + instances_query: Query<( + Entity, + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + directional_lights_query: Query<(Entity, &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; + + let mut this_frame_entity_to_light_id = EntityHashMap::::default(); + let previous_frame_light_entities: Vec<_> = raytracing_scene_bindings + .previous_frame_light_entities + .drain(..) + .collect(); + + 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 previous_frame_light_id_translations = 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(TEXTURE_MAP_NONE), + } + }; + 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 (entity, 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, + )); + + this_frame_entity_to_light_id.insert(entity, light_sources.get().len() as u32 - 1); + raytracing_scene_bindings + .previous_frame_light_entities + .push(entity); + } + + instance_id += 1; + } + + if instance_id == 0 { + return; + } + + for (entity, 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)); + + this_frame_entity_to_light_id.insert(entity, light_sources.get().len() as u32 - 1); + raytracing_scene_bindings + .previous_frame_light_entities + .push(entity); + } + + for previous_frame_light_entity in previous_frame_light_entities { + let current_frame_index = this_frame_entity_to_light_id + .get(&previous_frame_light_entity) + .copied() + .unwrap_or(LIGHT_NOT_PRESENT_THIS_FRAME); + previous_frame_light_id_translations + .get_mut() + .push(current_frame_index); + } + + 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); + previous_frame_light_id_translations.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(), + previous_frame_light_id_translations.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), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + previous_frame_light_entities: Vec::new(), + } + } +} + +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..eeed96ad8e --- /dev/null +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -0,0 +1,167 @@ +#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, +} + +const LIGHT_NOT_PRESENT_THIS_FRAME = 0xFFFFFFFFu; + +@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; +@group(0) @binding(11) var previous_frame_light_id_translations: 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..be709f0bc8 --- /dev/null +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -0,0 +1,199 @@ +#define_import_path bevy_solari::sampling + +#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u} +#import bevy_render::maths::{PI, 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); +} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec19%3A294 +fn sample_disk(disk_radius: f32, rng: ptr) -> vec2 { + let ab = 2.0 * rand_vec2f(rng) - 1.0; + let a = ab.x; + var b = ab.y; + if (b == 0.0) { b = 1.0; } + + var phi: f32; + var r: f32; + if (a * a > b * b) { + r = disk_radius * a; + phi = (PI / 4.0) * (b / a); + } else { + r = disk_radius * b; + phi = (PI / 2.0) - (PI / 4.0) * (a / b); + } + + let x = r * cos(phi); + let y = r * sin(phi); + return vec2(x, y); +} + +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 8fa5bae2cc..1538ac1ebc 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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 4b2d206420..fab5b2d993 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -2,8 +2,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Provides 2D sprite rendering functionality. @@ -16,6 +16,7 @@ mod picking_backend; mod render; mod sprite; mod texture_slice; +mod tilemap_chunk; /// The sprite prelude. /// @@ -40,18 +41,20 @@ 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::{load_internal_asset, weak_handle, AssetEventSystems, Assets, Handle}; +use bevy_asset::{embedded_asset, AssetEventSystems, Assets}; use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; use bevy_ecs::prelude::*; use bevy_image::{prelude::*, TextureAtlasPlugin}; use bevy_render::{ batching::sort_binned_render_phase, - mesh::{Mesh, Mesh2d, MeshAabb}, - primitives::Aabb, + load_shader_library, + mesh::{Mesh, Mesh2d}, + primitives::{Aabb, MeshAabb}, render_phase::AddRenderCommand, - render_resource::{Shader, SpecializedRenderPipelines}, + render_resource::SpecializedRenderPipelines, view::{NoFrustumCulling, VisibilitySystems}, ExtractSchedule, Render, RenderApp, RenderSystems, }; @@ -60,11 +63,6 @@ use bevy_render::{ #[derive(Default)] pub struct SpritePlugin; -pub const SPRITE_SHADER_HANDLE: Handle = - weak_handle!("ed996613-54c0-49bd-81be-1c2d1a0d03c2"); -pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("43947210-8df6-459a-8f2a-12f350d174cc"); - /// System set for sprite rendering. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum SpriteSystems { @@ -78,18 +76,9 @@ pub type SpriteSystem = SpriteSystems; impl Plugin for SpritePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - SPRITE_SHADER_HANDLE, - "render/sprite.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SPRITE_VIEW_BINDINGS_SHADER_HANDLE, - "render/sprite_view_bindings.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "render/sprite_view_bindings.wgsl"); + + embedded_asset!(app, "render/sprite.wgsl"); if !app.is_plugin_added::() { app.add_plugins(TextureAtlasPlugin); @@ -100,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, ( @@ -169,9 +163,9 @@ pub fn calculate_bounds_2d( atlases: Res>, meshes_without_aabb: Query<(Entity, &Mesh2d), (Without, Without)>, sprites_to_recalculate_aabb: Query< - (Entity, &Sprite), + (Entity, &Sprite, &Anchor), ( - Or<(Without, Changed)>, + Or<(Without, Changed, Changed)>, Without, ), >, @@ -183,7 +177,7 @@ pub fn calculate_bounds_2d( } } } - for (entity, sprite) in &sprites_to_recalculate_aabb { + for (entity, sprite, anchor) in &sprites_to_recalculate_aabb { if let Some(size) = sprite .custom_size .or_else(|| sprite.rect.map(|rect| rect.size())) @@ -197,7 +191,7 @@ pub fn calculate_bounds_2d( }) { let aabb = Aabb { - center: (-sprite.anchor.as_vec() * size).extend(0.0).into(), + center: (-anchor.as_vec() * size).extend(0.0).into(), half_extents: (0.5 * size).extend(0.0).into(), }; commands.entity(entity).try_insert(aabb); @@ -334,12 +328,14 @@ mod test { // Add entities let entity = app .world_mut() - .spawn(Sprite { - rect: Some(Rect::new(0., 0., 0.5, 1.)), - anchor: Anchor::TOP_RIGHT, - image: image_handle, - ..default() - }) + .spawn(( + Sprite { + rect: Some(Rect::new(0., 0., 0.5, 1.)), + image: image_handle, + ..default() + }, + Anchor::TOP_RIGHT, + )) .id(); // Create AABB diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 83b6930776..d814cfc384 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,26 +1,18 @@ use crate::{AlphaMode2d, Material2d, Material2dPlugin}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Asset, AssetApp, Assets, Handle}; +use bevy_asset::{embedded_asset, embedded_path, Asset, AssetApp, AssetPath, Assets, Handle}; use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; use bevy_image::Image; use bevy_math::{Affine2, Mat3, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage}; -pub const COLOR_MATERIAL_SHADER_HANDLE: Handle = - weak_handle!("92e0e6e9-ed0b-4db3-89ab-5f65d3678250"); - #[derive(Default)] pub struct ColorMaterialPlugin; impl Plugin for ColorMaterialPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - COLOR_MATERIAL_SHADER_HANDLE, - "color_material.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "color_material.wgsl"); app.add_plugins(Material2dPlugin::::default()) .register_asset_reflect::(); @@ -152,7 +144,9 @@ impl AsBindGroupShaderType for ColorMaterial { impl Material2d for ColorMaterial { fn fragment_shader() -> ShaderRef { - COLOR_MATERIAL_SHADER_HANDLE.into() + ShaderRef::Path( + AssetPath::from_path_buf(embedded_path!("color_material.wgsl")).with_source("embedded"), + ) } fn alpha_mode(&self) -> AlphaMode2d { diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 3f76b516cd..06914690ca 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -410,7 +410,7 @@ where fn clone(&self) -> Self { Self { mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data.clone(), + bind_group_data: self.bind_group_data, } } } @@ -753,7 +753,7 @@ pub fn specialize_material2d_meshes( &material2d_pipeline, Material2dKey { mesh_key, - bind_group_data: material_2d.key.clone(), + bind_group_data: material_2d.key, }, &mesh.layout, ); @@ -967,7 +967,9 @@ impl RenderAsset for PreparedMaterial2d { transparent_draw_functions, material_param, ): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { + let bind_group_data = material.bind_group_data(); match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { Ok(prepared) => { let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); @@ -986,7 +988,7 @@ impl RenderAsset for PreparedMaterial2d { Ok(PreparedMaterial2d { bindings: prepared.bindings, bind_group: prepared.bind_group, - key: prepared.data, + key: bind_group_data, properties: Material2dProperties { depth_bias: material.depth_bias(), alpha_mode: material.alpha_mode(), diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 3bacc35194..08620c5a4d 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,5 +1,6 @@ use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetId, Handle}; +use bevy_render::load_shader_library; use crate::{tonemapping_pipeline_key, Material2dBindGroupId}; use bevy_core_pipeline::tonemapping::DebandDither; @@ -51,60 +52,22 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::components::GlobalTransform; +use bevy_utils::default; use nonmax::NonMaxU32; use tracing::error; #[derive(Default)] pub struct Mesh2dRenderPlugin; -pub const MESH2D_VERTEX_OUTPUT: Handle = - weak_handle!("71e279c7-85a0-46ac-9a76-1586cbf506d0"); -pub const MESH2D_VIEW_TYPES_HANDLE: Handle = - weak_handle!("01087b0d-91e9-46ac-8628-dfe19a7d4b83"); -pub const MESH2D_VIEW_BINDINGS_HANDLE: Handle = - weak_handle!("fbdd8b80-503d-4688-bcec-db29ab4620b2"); -pub const MESH2D_TYPES_HANDLE: Handle = - weak_handle!("199f2089-6e99-4348-9bb1-d82816640a7f"); -pub const MESH2D_BINDINGS_HANDLE: Handle = - weak_handle!("a7bd44cc-0580-4427-9a00-721cf386b6e4"); -pub const MESH2D_FUNCTIONS_HANDLE: Handle = - weak_handle!("0d08ff71-68c1-4017-83e2-bfc34d285c51"); -pub const MESH2D_SHADER_HANDLE: Handle = - weak_handle!("91a7602b-df95-4ea3-9d97-076abcb69d91"); - impl Plugin for Mesh2dRenderPlugin { fn build(&self, app: &mut bevy_app::App) { - load_internal_asset!( - app, - MESH2D_VERTEX_OUTPUT, - "mesh2d_vertex_output.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_VIEW_TYPES_HANDLE, - "mesh2d_view_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_VIEW_BINDINGS_HANDLE, - "mesh2d_view_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_TYPES_HANDLE, - "mesh2d_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_FUNCTIONS_HANDLE, - "mesh2d_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, MESH2D_SHADER_HANDLE, "mesh2d.wgsl", Shader::from_wgsl); + load_shader_library!(app, "mesh2d_vertex_output.wgsl"); + load_shader_library!(app, "mesh2d_view_types.wgsl"); + load_shader_library!(app, "mesh2d_view_bindings.wgsl"); + load_shader_library!(app, "mesh2d_types.wgsl"); + load_shader_library!(app, "mesh2d_functions.wgsl"); + + embedded_asset!(app, "mesh2d.wgsl"); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -168,13 +131,10 @@ impl Plugin for Mesh2dRenderPlugin { // Load the mesh_bindings shader module here as it depends on runtime information about // whether storage buffers are supported, or the maximum uniform buffer binding size. - load_internal_asset!( - app, - MESH2D_BINDINGS_HANDLE, - "mesh2d_bindings.wgsl", - Shader::from_wgsl_with_defs, - mesh_bindings_shader_defs - ); + load_shader_library!(app, "mesh2d_bindings.wgsl", move |settings| *settings = + ShaderSettings { + shader_defs: mesh_bindings_shader_defs.clone() + }); } } @@ -316,6 +276,7 @@ pub fn extract_mesh2d( pub struct Mesh2dPipeline { pub view_layout: BindGroupLayout, pub mesh_layout: BindGroupLayout, + pub shader: Handle, // This dummy white texture is to be used in place of optional textures pub dummy_white_gpu_image: GpuImage, pub per_object_buffer_batch_size: Option, @@ -333,19 +294,13 @@ impl FromWorld for Mesh2dPipeline { let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); let view_layout = render_device.create_bind_group_layout( "mesh2d_view_layout", - &BindGroupLayoutEntries::with_indices( + &BindGroupLayoutEntries::sequential( ShaderStages::VERTEX_FRAGMENT, ( - (0, uniform_buffer::(true)), - (1, uniform_buffer::(false)), - ( - 2, - tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), - ), - ( - 3, - tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), - ), + uniform_buffer::(true), + uniform_buffer::(false), + tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), + tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), ), ), ); @@ -397,6 +352,7 @@ impl FromWorld for Mesh2dPipeline { per_object_buffer_batch_size: GpuArrayBuffer::::batch_size( render_device, ), + shader: load_embedded_asset!(world, "mesh2d.wgsl"), } } } @@ -690,23 +646,22 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { Ok(RenderPipelineDescriptor { vertex: VertexState { - shader: MESH2D_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.shader.clone(), shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { - shader: MESH2D_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend, write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.mesh_layout.clone()], - push_constant_ranges: vec![], primitive: PrimitiveState { front_face: FrontFace::Ccw, cull_mode: None, @@ -738,7 +693,7 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { alpha_to_coverage_enabled: false, }, label: Some(label.into()), - zero_initialize_workgroup_memory: false, + ..default() }) } } @@ -794,11 +749,11 @@ pub fn prepare_mesh2d_view_bind_groups( let view_bind_group = render_device.create_bind_group( "mesh2d_view_bind_group", &mesh2d_pipeline.view_layout, - &BindGroupEntries::with_indices(( - (0, view_binding.clone()), - (1, globals.clone()), - (2, lut_bindings.0), - (3, lut_bindings.1), + &BindGroupEntries::sequential(( + view_binding.clone(), + globals.clone(), + lut_bindings.0, + lut_bindings.1, )), ); @@ -817,7 +772,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 468a47f6bb..f71d8c63f7 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ - load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, + embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, Assets, Handle, UntypedAssetId, }; use bevy_color::{Color, ColorToComponents}; @@ -36,7 +36,7 @@ use bevy_render::{ render_asset::{ prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_phase::{ AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, InputUniformIndex, PhaseItem, @@ -54,9 +54,6 @@ use bevy_render::{ use core::{hash::Hash, ops::Range}; use tracing::error; -pub const WIREFRAME_2D_SHADER_HANDLE: Handle = - weak_handle!("2d8a3853-2927-4de2-9dc7-3971e7e40970"); - /// A [`Plugin`] that draws wireframes for 2D meshes. /// /// Wireframes currently do not work when using webgl or webgpu. @@ -81,12 +78,7 @@ impl Wireframe2dPlugin { impl Plugin for Wireframe2dPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - WIREFRAME_2D_SHADER_HANDLE, - "wireframe2d.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "wireframe2d.wgsl"); app.add_plugins(( BinnedRenderPhasePlugin::::new(self.debug_flags), @@ -339,7 +331,7 @@ impl FromWorld for Wireframe2dPipeline { fn from_world(render_world: &mut World) -> Self { Wireframe2dPipeline { mesh_pipeline: render_world.resource::().clone(), - shader: WIREFRAME_2D_SHADER_HANDLE, + shader: load_embedded_asset!(render_world, "wireframe2d.wgsl"), } } } @@ -380,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) = @@ -481,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 56579c9c0a..cda9955b95 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -7,10 +7,10 @@ //! //! ## 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::Sprite; +use crate::{Anchor, Sprite}; use bevy_app::prelude::*; use bevy_asset::prelude::*; use bevy_color::Alpha; @@ -100,6 +100,7 @@ fn sprite_picking( Entity, &Sprite, &GlobalTransform, + &Anchor, &Pickable, &ViewVisibility, )>, @@ -107,9 +108,9 @@ fn sprite_picking( ) { let mut sorted_sprites: Vec<_> = sprite_query .iter() - .filter_map(|(entity, sprite, transform, pickable, vis)| { + .filter_map(|(entity, sprite, transform, anchor, pickable, vis)| { if !transform.affine().is_nan() && vis.get() { - Some((entity, sprite, transform, pickable)) + Some((entity, sprite, transform, anchor, pickable)) } else { None } @@ -117,7 +118,7 @@ fn sprite_picking( .collect(); // radsort is a stable radix sort that performed better than `slice::sort_by_key` - radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| { + radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _, _)| { -transform.translation().z }); @@ -144,13 +145,15 @@ fn sprite_picking( continue; }; - let viewport_pos = camera - .logical_viewport_rect() - .map(|v| v.min) - .unwrap_or_default(); - let pos_in_viewport = location.position - viewport_pos; + let viewport_pos = location.position; + if let Some(viewport) = camera.logical_viewport_rect() { + if !viewport.contains(viewport_pos) { + // The pointer is outside the viewport, skip it + continue; + } + } - let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else { + let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, viewport_pos) else { continue; }; let cursor_ray_len = cam_ortho.far - cam_ortho.near; @@ -159,7 +162,7 @@ fn sprite_picking( let picks: Vec<(Entity, HitData)> = sorted_sprites .iter() .copied() - .filter_map(|(entity, sprite, sprite_transform, pickable)| { + .filter_map(|(entity, sprite, sprite_transform, anchor, pickable)| { if blocked { return None; } @@ -192,6 +195,7 @@ fn sprite_picking( let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point( cursor_pos_sprite, + *anchor, &images, &texture_atlas_layout, ) else { diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index de57f43536..cabab135c2 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,7 +1,7 @@ use core::ops::Range; -use crate::{ComputedTextureSlices, ScalingMode, Sprite, SPRITE_SHADER_HANDLE}; -use bevy_asset::{AssetEvent, AssetId, Assets}; +use crate::{Anchor, ComputedTextureSlices, ScalingMode, Sprite}; +use bevy_asset::{load_embedded_asset, AssetEvent, AssetId, Assets, Handle}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_core_pipeline::{ core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, @@ -40,6 +40,7 @@ use bevy_render::{ Extract, }; use bevy_transform::components::GlobalTransform; +use bevy_utils::default; use bytemuck::{Pod, Zeroable}; use fixedbitset::FixedBitSet; @@ -47,6 +48,7 @@ use fixedbitset::FixedBitSet; pub struct SpritePipeline { view_layout: BindGroupLayout, material_layout: BindGroupLayout, + shader: Handle, pub dummy_white_gpu_image: GpuImage, } @@ -62,18 +64,12 @@ impl FromWorld for SpritePipeline { let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); let view_layout = render_device.create_bind_group_layout( "sprite_view_layout", - &BindGroupLayoutEntries::with_indices( + &BindGroupLayoutEntries::sequential( ShaderStages::VERTEX_FRAGMENT, ( - (0, uniform_buffer::(true)), - ( - 1, - tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), - ), - ( - 2, - tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), - ), + uniform_buffer::(true), + tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), + tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), ), ), ); @@ -124,6 +120,7 @@ impl FromWorld for SpritePipeline { view_layout, material_layout, dummy_white_gpu_image, + shader: load_embedded_asset!(world, "sprite.wgsl"), } } } @@ -267,31 +264,22 @@ impl SpecializedRenderPipeline for SpritePipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: SPRITE_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.shader.clone(), shader_defs: shader_defs.clone(), buffers: vec![instance_rate_vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { - shader: SPRITE_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.material_layout.clone()], - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - }, // Sprites are always alpha blended so they never need to write to depth. // They just need to read it in case an opaque mesh2d // that wrote to depth is present. @@ -317,8 +305,7 @@ impl SpecializedRenderPipeline for SpritePipeline { alpha_to_coverage_enabled: false, }, label: Some("sprite_pipeline".into()), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -394,13 +381,14 @@ pub fn extract_sprites( &ViewVisibility, &Sprite, &GlobalTransform, + &Anchor, Option<&ComputedTextureSlices>, )>, >, ) { extracted_sprites.sprites.clear(); extracted_slices.slices.clear(); - for (main_entity, render_entity, view_visibility, sprite, transform, slices) in + for (main_entity, render_entity, view_visibility, sprite, transform, anchor, slices) in sprite_query.iter() { if !view_visibility.get() { @@ -411,7 +399,7 @@ pub fn extract_sprites( let start = extracted_slices.slices.len(); extracted_slices .slices - .extend(slices.extract_slices(sprite)); + .extend(slices.extract_slices(sprite, anchor.as_vec())); let end = extracted_slices.slices.len(); extracted_sprites.sprites.push(ExtractedSprite { main_entity, @@ -451,7 +439,7 @@ pub fn extract_sprites( flip_y: sprite.flip_y, image_handle_id: sprite.image.id(), kind: ExtractedSpriteKind::Single { - anchor: sprite.anchor.as_vec(), + anchor: anchor.as_vec(), rect, scaling_mode: sprite.image_mode.scale(), // Pass the custom size @@ -633,11 +621,7 @@ pub fn prepare_sprite_view_bind_groups( let view_bind_group = render_device.create_bind_group( "mesh2d_view_bind_group", &sprite_pipeline.view_layout, - &BindGroupEntries::with_indices(( - (0, view_binding.clone()), - (1, lut_bindings.0), - (2, lut_bindings.1), - )), + &BindGroupEntries::sequential((view_binding.clone(), lut_bindings.0, lut_bindings.1)), ); commands.entity(entity).insert(SpriteViewBindGroup { @@ -905,7 +889,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>, @@ -922,7 +906,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>, @@ -952,7 +936,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/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 32b8ebb49e..39e215df04 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -15,7 +15,7 @@ use crate::TextureSlicer; /// Describes a sprite to be rendered to a 2D camera #[derive(Component, Debug, Default, Clone, Reflect)] -#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass)] +#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass, Anchor)] #[reflect(Component, Default, Debug, Clone)] #[component(on_add = view::add_visibility_class::)] pub struct Sprite { @@ -38,8 +38,6 @@ pub struct Sprite { /// When used with a [`TextureAtlas`], the rect /// is offset by the atlas's minimal (top-left) corner position. pub rect: Option, - /// [`Anchor`] point of the sprite in the world - pub anchor: Anchor, /// How the sprite's image will be scaled. pub image_mode: SpriteImageMode, } @@ -86,6 +84,7 @@ impl Sprite { pub fn compute_pixel_space_point( &self, point_relative_to_sprite: Vec2, + anchor: Anchor, images: &Assets, texture_atlases: &Assets, ) -> Result { @@ -112,7 +111,7 @@ impl Sprite { }; let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size()); - let sprite_center = -self.anchor.as_vec() * sprite_size; + let sprite_center = -anchor.as_vec() * sprite_size; let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center; @@ -125,12 +124,15 @@ impl Sprite { point_relative_to_sprite_center.y *= -1.0; } + if sprite_size.x == 0.0 || sprite_size.y == 0.0 { + return Err(point_relative_to_sprite_center); + } + let sprite_to_texture_ratio = { let texture_size = texture_rect.size(); - let div_or_zero = |a, b| if b == 0.0 { 0.0 } else { a / b }; Vec2::new( - div_or_zero(texture_size.x, sprite_size.x), - div_or_zero(texture_size.y, sprite_size.y), + texture_size.x / sprite_size.x, + texture_size.y / sprite_size.y, ) }; @@ -279,10 +281,10 @@ impl From for Anchor { mod tests { use bevy_asset::{Assets, RenderAssetUsages}; use bevy_color::Color; - use bevy_image::Image; + use bevy_image::{Image, ToExtents}; use bevy_image::{TextureAtlas, TextureAtlasLayout}; use bevy_math::{Rect, URect, UVec2, Vec2}; - use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat}; + use bevy_render::render_resource::{TextureDimension, TextureFormat}; use crate::Anchor; @@ -291,11 +293,7 @@ mod tests { /// Makes a new image of the specified size. fn make_image(size: UVec2) -> Image { Image::new_fill( - Extent3d { - width: size.x, - height: size.y, - depth_or_array_layers: 1, - }, + size.to_extents(), TextureDimension::D2, &[0, 0, 0, 255], TextureFormat::Rgba8Unorm, @@ -315,8 +313,14 @@ mod tests { ..Default::default() }; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point( + point, + Anchor::default(), + &image_assets, + &texture_atlas_assets, + ) + }; assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5))); assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5))); @@ -334,7 +338,12 @@ mod tests { let compute = |point| { sprite - .compute_pixel_space_point(point, &image_assets, &texture_atlas_assets) + .compute_pixel_space_point( + point, + Anchor::default(), + &image_assets, + &texture_atlas_assets, + ) // Round to remove floating point errors. .map(|x| (x * 1e5).round() / 1e5) .map_err(|x| (x * 1e5).round() / 1e5) @@ -355,12 +364,13 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5))); assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5))); @@ -377,12 +387,13 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::TOP_RIGHT, ..Default::default() }; + let anchor = Anchor::TOP_RIGHT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5))); assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5))); @@ -399,13 +410,14 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, flip_x: true, ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5))); assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5))); @@ -422,13 +434,14 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::TOP_RIGHT, flip_y: true, ..Default::default() }; + let anchor = Anchor::TOP_RIGHT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5))); assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5))); @@ -446,12 +459,13 @@ mod tests { let sprite = Sprite { image, rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)), - anchor: Anchor::BOTTOM_LEFT, ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0))); // The pixel is outside the rect, but is still a valid pixel in the image. assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0))); @@ -470,16 +484,17 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, texture_atlas: Some(TextureAtlas { layout: texture_atlas, index: 0, }), ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5))); // The pixel is outside the texture atlas, but is still a valid pixel in the image. assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5))); @@ -498,7 +513,6 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, texture_atlas: Some(TextureAtlas { layout: texture_atlas, index: 0, @@ -507,9 +521,11 @@ mod tests { rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)), ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5))); // The pixel is outside the texture atlas, but is still a valid pixel in the image. assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5))); @@ -529,11 +545,47 @@ mod tests { ..Default::default() }; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point( + point, + Anchor::default(), + &image_assets, + &texture_atlas_assets, + ) + }; assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0))); assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0))); // The pixel is outside the texture atlas, but is still a valid pixel in the image. assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(2.5, -1.0))); } + + #[test] + fn compute_pixel_space_point_for_sprite_with_zero_custom_size() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + custom_size: Some(Vec2::new(0.0, 0.0)), + ..Default::default() + }; + + let compute = |point| { + sprite.compute_pixel_space_point( + point, + Anchor::default(), + &image_assets, + &texture_atlas_assets, + ) + }; + assert_eq!(compute(Vec2::new(30.0, 15.0)), Err(Vec2::new(30.0, -15.0))); + assert_eq!( + compute(Vec2::new(-10.0, -15.0)), + Err(Vec2::new(-10.0, 15.0)) + ); + // The pixel is outside the texture atlas, but is still a valid pixel in the image. + assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(0.0, -35.0))); + } } diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index f36cf4bfac..d4972f0384 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -1,6 +1,5 @@ -use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout}; - use super::TextureSlice; +use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout}; use bevy_asset::{AssetEvent, Assets}; use bevy_ecs::prelude::*; use bevy_image::Image; @@ -23,6 +22,7 @@ impl ComputedTextureSlices { pub(crate) fn extract_slices<'a>( &'a self, sprite: &'a Sprite, + anchor: Vec2, ) -> impl ExactSizeIterator + 'a { let mut flip = Vec2::ONE; if sprite.flip_x { @@ -31,7 +31,7 @@ impl ComputedTextureSlices { if sprite.flip_y { flip.y *= -1.0; } - let anchor = sprite.anchor.as_vec() + let anchor = anchor * sprite .custom_size .unwrap_or(sprite.rect.unwrap_or_default().size()); 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..174816154b --- /dev/null +++ b/crates/bevy_sprite/src/tilemap_chunk/mod.rs @@ -0,0 +1,267 @@ +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, ToExtents}; +use bevy_math::{FloatOrd, UVec2, Vec2, Vec3}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + mesh::{Indices, Mesh, Mesh2d, PrimitiveTopology}, + render_resource::{ + TextureDataOrder, 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(), + ), + data_order: TextureDataOrder::default(), + texture_descriptor: TextureDescriptor { + size: size.to_extents(), + 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 1ae52fa571..17f9d6c787 100644 --- a/crates/bevy_state/Cargo.toml +++ b/crates/bevy_state/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_state" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Finite state machines for Bevy" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -30,7 +30,6 @@ bevy_app = ["dep:bevy_app"] ## supported platforms. std = [ "bevy_ecs/std", - "bevy_utils/std", "bevy_reflect?/std", "bevy_app?/std", "bevy_platform/std", @@ -40,7 +39,6 @@ std = [ ## on all platforms, including `no_std`. critical-section = [ "bevy_ecs/critical-section", - "bevy_utils/critical-section", "bevy_app?/critical-section", "bevy_reflect?/critical-section", "bevy_platform/critical-section", @@ -48,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..a4ff416632 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,11 +9,10 @@ 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" -proc-macro2 = "1.0" [lints] workspace = true diff --git a/crates/bevy_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs index f461f0ead2..3c5a2d0674 100644 --- a/crates/bevy_state/macros/src/lib.rs +++ b/crates/bevy_state/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 `States` and `SubStates` traits. + extern crate proc_macro; mod states; @@ -8,11 +9,15 @@ mod states; use bevy_macro_utils::BevyManifest; use proc_macro::TokenStream; +/// Implements the `States` trait for a type - see the trait +/// docs for an example usage. #[proc_macro_derive(States, attributes(states))] pub fn derive_states(input: TokenStream) -> TokenStream { states::derive_states(input) } +/// Implements the `SubStates` trait for a type - see the trait +/// docs for an example usage. #[proc_macro_derive(SubStates, attributes(states, source))] pub fn derive_substates(input: TokenStream) -> TokenStream { states::derive_substates(input) diff --git a/crates/bevy_state/macros/src/states.rs b/crates/bevy_state/macros/src/states.rs index 52c133f6ee..57c997156b 100644 --- a/crates/bevy_state/macros/src/states.rs +++ b/crates/bevy_state/macros/src/states.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, LitBool, Pat, Path, Result}; use crate::bevy_state_path; @@ -13,14 +13,16 @@ struct StatesAttrs { fn parse_states_attr(ast: &DeriveInput) -> Result { let mut attrs = StatesAttrs { - scoped_entities_enabled: false, + scoped_entities_enabled: true, }; for attr in ast.attrs.iter() { if attr.path().is_ident(STATES) { attr.parse_nested_meta(|nested| { if nested.path.is_ident(SCOPED_ENTITIES) { - attrs.scoped_entities_enabled = true; + if let Ok(value) = nested.value() { + attrs.scoped_entities_enabled = value.parse::()?.value(); + } Ok(()) } else { Err(nested.error("Unsupported attribute")) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 903a098137..27bfd826e1 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -25,7 +25,7 @@ pub trait AppExtStates { /// These schedules are triggered before [`Update`](bevy_app::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can - /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`Condition`](bevy_ecs::prelude::Condition). + /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`SystemCondition`](bevy_ecs::prelude::SystemCondition). /// /// Note that you can also apply state transitions at other points in the schedule /// by triggering the [`StateTransition`](struct@StateTransition) schedule manually. @@ -41,7 +41,7 @@ pub trait AppExtStates { /// These schedules are triggered before [`Update`](bevy_app::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can - /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`Condition`](bevy_ecs::prelude::Condition). + /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`SystemCondition`](bevy_ecs::prelude::SystemCondition). /// /// Note that you can also apply state transitions at other points in the schedule /// by triggering the [`StateTransition`](struct@StateTransition) schedule manually. @@ -59,10 +59,11 @@ pub trait AppExtStates { /// Enable state-scoped entity clearing for state `S`. /// - /// If the [`States`] trait was derived with the `#[states(scoped_entities)]` attribute, it - /// will be called automatically. + /// This is enabled by default. If you don't want this behavior, add the `#[states(scoped_entities = false)]` + /// attribute when deriving the [`States`] trait. /// /// For more information refer to [`crate::state_scoped`]. + #[doc(hidden)] fn enable_state_scoped_entities(&mut self) -> &mut Self; #[cfg(feature = "bevy_reflect")] @@ -106,7 +107,7 @@ impl AppExtStates for SubApp { ); S::register_state(schedule); let state = self.world().resource::>().get().clone(); - self.world_mut().send_event(StateTransitionEvent { + self.world_mut().write_event(StateTransitionEvent { exited: None, entered: Some(state), }); @@ -115,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 @@ -131,7 +132,7 @@ impl AppExtStates for SubApp { "The `StateTransition` schedule is missing. Did you forget to add StatesPlugin or DefaultPlugins before calling insert_state?" ); S::register_state(schedule); - self.world_mut().send_event(StateTransitionEvent { + self.world_mut().write_event(StateTransitionEvent { exited: None, entered: Some(state), }); @@ -144,7 +145,7 @@ impl AppExtStates for SubApp { self.world_mut() .resource_mut::>>() .clear(); - self.world_mut().send_event(StateTransitionEvent { + self.world_mut().write_event(StateTransitionEvent { exited: None, entered: Some(state), }); @@ -168,7 +169,7 @@ impl AppExtStates for SubApp { .world() .get_resource::>() .map(|s| s.get().clone()); - self.world_mut().send_event(StateTransitionEvent { + self.world_mut().write_event(StateTransitionEvent { exited: None, entered: state, }); @@ -177,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 @@ -199,7 +200,7 @@ impl AppExtStates for SubApp { .world() .get_resource::>() .map(|s| s.get().clone()); - self.world_mut().send_event(StateTransitionEvent { + self.world_mut().write_event(StateTransitionEvent { exited: None, entered: state, }); @@ -208,19 +209,20 @@ 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 } + #[doc(hidden)] fn enable_state_scoped_entities(&mut self) -> &mut Self { if !self .world() .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 @@ -285,6 +287,7 @@ impl AppExtStates for App { self } + #[doc(hidden)] fn enable_state_scoped_entities(&mut self) -> &mut Self { self.main_mut().enable_state_scoped_entities::(); self 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/condition.rs b/crates/bevy_state/src/condition.rs index faede71be5..4d9acb8cfe 100644 --- a/crates/bevy_state/src/condition.rs +++ b/crates/bevy_state/src/condition.rs @@ -1,7 +1,7 @@ use crate::state::{State, States}; use bevy_ecs::{change_detection::DetectChanges, system::Res}; -/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// A [`SystemCondition`](bevy_ecs::prelude::SystemCondition)-satisfying system that returns `true` /// if the state machine exists. /// /// # Example @@ -48,7 +48,7 @@ pub fn state_exists(current_state: Option>>) -> bool { current_state.is_some() } -/// Generates a [`Condition`](bevy_ecs::prelude::Condition)-satisfying closure that returns `true` +/// Generates a [`SystemCondition`](bevy_ecs::prelude::SystemCondition)-satisfying closure that returns `true` /// if the state machine is currently in `state`. /// /// Will return `false` if the state does not exist or if not in `state`. @@ -107,7 +107,7 @@ pub fn in_state(state: S) -> impl FnMut(Option>>) -> boo } } -/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// A [`SystemCondition`](bevy_ecs::prelude::SystemCondition)-satisfying system that returns `true` /// if the state machine changed state. /// /// To do things on transitions to/from specific states, use their respective OnEnter/OnExit @@ -171,7 +171,7 @@ pub fn state_changed(current_state: Option>>) -> bool { #[cfg(test)] mod tests { - use bevy_ecs::schedule::{Condition, IntoScheduleConfigs, Schedule}; + use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule, SystemCondition}; use crate::prelude::*; use bevy_state_macros::States; 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..60e8f7d850 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -21,7 +21,7 @@ mod sealed { /// A [`States`] type or tuple of types which implement [`States`]. /// -/// This trait is used allow implementors of [`States`], as well +/// This trait is used to allow implementors of [`States`], as well /// as tuples containing exclusively implementors of [`States`], to /// be used as [`ComputedStates::SourceStates`]. /// @@ -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/sub_states.rs b/crates/bevy_state/src/state/sub_states.rs index 745c4baf0b..c6844eed28 100644 --- a/crates/bevy_state/src/state/sub_states.rs +++ b/crates/bevy_state/src/state/sub_states.rs @@ -7,7 +7,7 @@ pub use bevy_state_macros::SubStates; /// but unlike [`ComputedStates`](crate::state::ComputedStates) - while they exist they can be manually modified. /// /// The default approach to creating [`SubStates`] is using the derive macro, and defining a single source state -/// and value to determine it's existence. +/// and value to determine its existence. /// /// ``` /// # use bevy_ecs::prelude::*; diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index dfe711f245..90b033b7fd 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, @@ -50,16 +50,18 @@ pub struct OnTransition { /// } /// ``` /// +/// This schedule is split up into four phases, as described in [`StateTransitionSteps`]. +/// /// [`PreStartup`]: https://docs.rs/bevy/latest/bevy/prelude/struct.PreStartup.html /// [`PreUpdate`]: https://docs.rs/bevy/latest/bevy/prelude/struct.PreUpdate.html #[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.rs b/crates/bevy_state/src/state_scoped.rs index c591d0c108..1abf0975ea 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -14,8 +14,7 @@ use crate::state::{StateTransitionEvent, States}; /// Entities marked with this component will be removed /// when the world's state of the matching type no longer matches the supplied value. /// -/// To enable this feature remember to add the attribute `#[states(scoped_entities)]` when deriving [`States`]. -/// It's also possible to enable it when adding the state to an app with [`enable_state_scoped_entities`](crate::app::AppExtStates::enable_state_scoped_entities). +/// If you need to disable this behavior, add the attribute `#[states(scoped_entities = false)]` when deriving [`States`]. /// /// ``` /// use bevy_state::prelude::*; @@ -23,7 +22,6 @@ use crate::state::{StateTransitionEvent, States}; /// use bevy_ecs::system::ScheduleSystem; /// /// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// #[states(scoped_entities)] /// enum GameState { /// #[default] /// MainMenu, @@ -44,7 +42,6 @@ use crate::state::{StateTransitionEvent, States}; /// # struct AppMock; /// # impl AppMock { /// # fn init_state(&mut self) {} -/// # fn enable_state_scoped_entities(&mut self) {} /// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} /// # } /// # struct Update; @@ -123,14 +120,12 @@ pub fn despawn_entities_on_exit_state( /// # struct AppMock; /// # impl AppMock { /// # fn init_state(&mut self) {} -/// # fn enable_state_scoped_entities(&mut self) {} /// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} /// # } /// # struct Update; /// # let mut app = AppMock; /// /// app.init_state::(); -/// app.enable_state_scoped_entities::(); /// app.add_systems(OnEnter(GameState::InGame), spawn_player); /// ``` #[derive(Component, Clone)] diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index 9defa4d888..a84490f64c 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -3,36 +3,50 @@ 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, }; use bevy_platform::collections::HashMap; -use crate::state::{FreelyMutableState, OnExit, StateTransitionEvent}; +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(); } } -#[derive(Resource)] -struct StateScopedEvents { - cleanup_fns: HashMap>, +#[derive(Copy, Clone)] +enum TransitionType { + OnExit, + OnEnter, } -impl StateScopedEvents { - fn add_event(&mut self, state: S) { - self.cleanup_fns - .entry(state) - .or_default() - .push(clear_event_queue::); +#[derive(Resource)] +struct StateScopedEvents { + /// Keeps track of which events need to be reset when the state is exited. + on_exit: HashMap>, + /// Keeps track of which events need to be reset when the state is entered. + on_enter: HashMap>, +} + +impl StateScopedEvents { + 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, + }; + map.entry(state).or_default().push(clear_event_queue::); } - fn cleanup(&self, w: &mut World, state: S) { - let Some(fns) = self.cleanup_fns.get(&state) else { + fn cleanup(&self, w: &mut World, state: S, transition_type: TransitionType) { + let map = match transition_type { + TransitionType::OnExit => &self.on_exit, + TransitionType::OnEnter => &self.on_enter, + }; + let Some(fns) = map.get(&state) else { return; }; for callback in fns { @@ -41,15 +55,16 @@ impl StateScopedEvents { } } -impl Default for StateScopedEvents { +impl Default for StateScopedEvents { fn default() -> Self { Self { - cleanup_fns: HashMap::default(), + on_exit: HashMap::default(), + on_enter: HashMap::default(), } } } -fn cleanup_state_scoped_event( +fn clear_events_on_exit_state( mut c: Commands, mut transitions: EventReader>, ) { @@ -65,48 +80,186 @@ fn cleanup_state_scoped_event( c.queue(move |w: &mut World| { w.resource_scope::, ()>(|w, events| { - events.cleanup(w, exited); + events.cleanup(w, exited, TransitionType::OnExit); }); }); } -fn add_state_scoped_event_impl( +fn clear_events_on_enter_state( + mut c: Commands, + mut transitions: EventReader>, +) { + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited { + return; + } + let Some(entered) = transition.entered.clone() else { + return; + }; + + c.queue(move |w: &mut World| { + w.resource_scope::, ()>(|w, events| { + events.cleanup(w, entered, TransitionType::OnEnter); + }); + }); +} + +fn clear_events_on_state_transition( app: &mut SubApp, _p: PhantomData, state: S, + transition_type: TransitionType, ) { if !app.world().contains_resource::>() { app.init_resource::>(); } - app.add_event::(); app.world_mut() .resource_mut::>() - .add_event::(state.clone()); - app.add_systems(OnExit(state), cleanup_state_scoped_event::); + .add_event::(state.clone(), transition_type); + match transition_type { + TransitionType::OnExit => app.add_systems(OnExit(state), clear_events_on_exit_state::), + TransitionType::OnEnter => { + app.add_systems(OnEnter(state), clear_events_on_enter_state::) + } + }; } /// Extension trait for [`App`] adding methods for registering state scoped events. pub trait StateScopedEventsAppExt { - /// Adds an [`Event`] that is automatically cleaned up when leaving the specified `state`. + /// Clears an [`BufferedEvent`] when exiting the specified `state`. /// - /// Note that event cleanup is ordered ambiguously relative to [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) - /// and [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity - /// cleanup and the [`OnExit`] schedule for the target state. All of these (state scoped - /// entities and events cleanup, and `OnExit`) occur within schedule [`StateTransition`](crate::prelude::StateTransition) + /// Note that event cleanup is ambiguously ordered relative to + /// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup, + /// and the [`OnExit`] schedule for the target state. + /// All of these (state scoped entities and events cleanup, and `OnExit`) + /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::ExitSchedules`. - fn add_state_scoped_event(&mut self, state: impl FreelyMutableState) -> &mut Self; + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; + + /// Clears an [`BufferedEvent`] when entering the specified `state`. + /// + /// Note that event cleanup is ambiguously ordered relative to + /// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup, + /// and the [`OnEnter`] schedule for the target state. + /// 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; } impl StateScopedEventsAppExt for App { - fn add_state_scoped_event(&mut self, state: impl FreelyMutableState) -> &mut Self { - add_state_scoped_event_impl(self.main_mut(), PhantomData::, state); + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition( + self.main_mut(), + PhantomData::, + state, + TransitionType::OnExit, + ); + self + } + + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition( + self.main_mut(), + PhantomData::, + state, + TransitionType::OnEnter, + ); self } } impl StateScopedEventsAppExt for SubApp { - fn add_state_scoped_event(&mut self, state: impl FreelyMutableState) -> &mut Self { - add_state_scoped_event_impl(self, PhantomData::, state); + 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 { + clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnEnter); self } } + +#[cfg(test)] +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)] + enum TestState { + #[default] + A, + B, + } + + #[derive(Event, BufferedEvent, Debug)] + struct StandardEvent; + + #[derive(Event, BufferedEvent, Debug)] + struct StateScopedEvent; + + #[test] + fn clear_event_on_exit_state() { + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.init_state::(); + + app.add_event::(); + app.add_event::() + .clear_events_on_exit_state::(TestState::A); + + app.world_mut().write_event(StandardEvent).unwrap(); + app.world_mut().write_event(StateScopedEvent).unwrap(); + assert!(!app.world().resource::>().is_empty()); + assert!(!app + .world() + .resource::>() + .is_empty()); + + app.world_mut() + .resource_mut::>() + .set(TestState::B); + app.update(); + + assert!(!app.world().resource::>().is_empty()); + assert!(app + .world() + .resource::>() + .is_empty()); + } + + #[test] + fn clear_event_on_enter_state() { + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.init_state::(); + + app.add_event::(); + app.add_event::() + .clear_events_on_enter_state::(TestState::B); + + app.world_mut().write_event(StandardEvent).unwrap(); + app.world_mut().write_event(StateScopedEvent).unwrap(); + assert!(!app.world().resource::>().is_empty()); + assert!(!app + .world() + .resource::>() + .is_empty()); + + app.world_mut() + .resource_mut::>() + .set(TestState::B); + app.update(); + + assert!(!app.world().resource::>().is_empty()); + assert!(app + .world() + .resource::>() + .is_empty()); + } +} diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 07c20b9750..e28d7fc88d 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -15,7 +15,12 @@ default = ["std", "async_executor"] ## Enables multi-threading support. ## Without this feature, all tasks will be run on a single thread. -multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"] +multi_threaded = [ + "std", + "dep:async-channel", + "dep:concurrent-queue", + "async_executor", +] ## Uses `async-executor` as a task execution backend. ## This backend is incompatible with `no_std` targets. @@ -42,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", ] } @@ -50,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/README.md b/crates/bevy_tasks/README.md index b03d2fcf97..04815df35e 100644 --- a/crates/bevy_tasks/README.md +++ b/crates/bevy_tasks/README.md @@ -38,6 +38,6 @@ The determining factor for what kind of work should go in each pool is latency r To enable `no_std` support in this crate, you will need to disable default features, and enable the `edge_executor` and `critical-section` features. -[bevy]: https://bevyengine.org +[bevy]: https://bevy.org [rayon]: https://github.com/rayon-rs/rayon [async-executor]: https://github.com/stjepang/async-executor 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 ae684a4eb5..66899ef36f 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -1,8 +1,8 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -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 5134ecda84..58bcfb1c5b 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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", ] } @@ -36,7 +36,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea cosmic-text = { version = "0.14", features = ["shape-run-cache"] } thiserror = { version = "2", default-features = false } serde = { version = "1", features = ["derive"] } -smallvec = "1.13" +smallvec = { version = "1", default-features = false } unicode-bidi = "0.3.13" sys-locale = "0.3.0" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_text/src/bounds.rs b/crates/bevy_text/src/bounds.rs index db2ceb0b28..1c0833b443 100644 --- a/crates/bevy_text/src/bounds.rs +++ b/crates/bevy_text/src/bounds.rs @@ -5,7 +5,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// The maximum width and height of text. The text will wrap according to the specified size. /// /// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the -/// specified [`JustifyText`](crate::text::JustifyText). +/// specified [`Justify`](crate::text::Justify). /// /// Note: only characters that are completely out of the bounds will be truncated, so this is not a /// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index a10dee5923..5407d25f84 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,10 +1,10 @@ use bevy_asset::{Assets, Handle}; -use bevy_image::{prelude::*, ImageSampler}; +use bevy_image::{prelude::*, ImageSampler, ToExtents}; use bevy_math::{IVec2, UVec2}; use bevy_platform::collections::HashMap; use bevy_render::{ render_asset::RenderAssetUsages, - render_resource::{Extent3d, TextureDimension, TextureFormat}, + render_resource::{TextureDimension, TextureFormat}, }; use crate::{FontSmoothing, GlyphAtlasLocation, TextError}; @@ -41,11 +41,7 @@ impl FontAtlas { font_smoothing: FontSmoothing, ) -> FontAtlas { let mut image = Image::new_fill( - Extent3d { - width: size.x, - height: size.y, - depth_or_array_layers: 1, - }, + size.to_extents(), TextureDimension::D2, &[0, 0, 0, 0], TextureFormat::Rgba8UnormSrgb, diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 8d32127c38..8f0dd91168 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -172,8 +172,8 @@ impl FontAtlasSet { .get_glyph_index(cache_key) .map(|location| GlyphAtlasInfo { location, - texture_atlas: atlas.texture_atlas.clone_weak(), - texture: atlas.texture.clone_weak(), + texture_atlas: atlas.texture_atlas.id(), + texture: atlas.texture.id(), }) }) }) diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index c761bc0033..5bc2111dbd 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -1,6 +1,6 @@ //! This module exports types related to rendering glyphs. -use bevy_asset::Handle; +use bevy_asset::AssetId; use bevy_image::prelude::*; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; @@ -23,7 +23,7 @@ pub struct PositionedGlyph { pub span_index: usize, /// The index of the glyph's line. pub line_index: usize, - /// The byte index of the glyph in it's line. + /// The byte index of the glyph in its line. pub byte_index: usize, /// The byte length of the glyph. pub byte_length: usize, @@ -38,14 +38,15 @@ pub struct PositionedGlyph { #[derive(Debug, Clone, Reflect)] #[reflect(Clone)] pub struct GlyphAtlasInfo { - /// A handle to the [`Image`] data for the texture atlas this glyph was placed in. + /// An asset ID to the [`Image`] data for the texture atlas this glyph was placed in. /// - /// A (weak) clone of the handle held by the [`FontAtlas`](crate::FontAtlas). - pub texture: Handle, - /// A handle to the [`TextureAtlasLayout`] map for the texture atlas this glyph was placed in. + /// An asset ID of the handle held by the [`FontAtlas`](crate::FontAtlas). + pub texture: AssetId, + /// An asset ID to the [`TextureAtlasLayout`] map for the texture atlas this glyph was placed + /// in. /// - /// A (weak) clone of the handle held by the [`FontAtlas`](crate::FontAtlas). - pub texture_atlas: Handle, + /// An asset ID of the handle held by the [`FontAtlas`](crate::FontAtlas). + pub texture_atlas: AssetId, /// Location and offset of a glyph within the texture atlas. pub location: GlyphAtlasLocation, } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 70ac992924..b36f5fa2bb 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -61,7 +61,7 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, JustifyText, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, + Font, Justify, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, TextFont, TextLayout, TextSpan, }; } @@ -87,18 +87,6 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); #[derive(Default)] pub struct TextPlugin; -/// Text is rendered for two different view projections; -/// 2-dimensional text ([`Text2d`]) is rendered in "world space" with a `BottomToTop` Y-axis, -/// while UI is rendered with a `TopToBottom` Y-axis. -/// This matters for text because the glyph positioning is different in either layout. -/// For `TopToBottom`, 0 is the top of the text, while for `BottomToTop` 0 is the bottom. -pub enum YAxisOrientation { - /// Top to bottom Y-axis orientation, for UI - TopToBottom, - /// Bottom to top Y-axis orientation, for 2d world space - BottomToTop, -} - /// System set in [`PostUpdate`] where all 2d text update systems are executed. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub struct Text2dUpdateSystems; diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 2a47866f76..8c1136c063 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -16,8 +16,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText, - LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, YAxisOrientation, + error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, + PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -53,11 +53,15 @@ impl Default for SwashCache { /// Information about a font collected as part of preparing for text layout. #[derive(Clone)] -struct FontFaceInfo { - stretch: cosmic_text::fontdb::Stretch, - style: cosmic_text::fontdb::Style, - weight: cosmic_text::fontdb::Weight, - family_name: Arc, +pub struct FontFaceInfo { + /// Width class: + pub stretch: cosmic_text::fontdb::Stretch, + /// Allows italic or oblique faces to be selected + pub style: cosmic_text::fontdb::Style, + /// The degree of blackness or stroke thickness + pub weight: cosmic_text::fontdb::Weight, + /// Font family name + pub family_name: Arc, } /// The `TextPipeline` is used to layout and render text blocks (see `Text`/[`Text2d`](crate::Text2d)). @@ -66,7 +70,7 @@ struct FontFaceInfo { #[derive(Default, Resource)] pub struct TextPipeline { /// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its [`Font`] [`Asset`](bevy_asset::Asset). - map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, Arc)>, + pub map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, Arc)>, /// Buffered vec for collecting spans. /// /// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10). @@ -84,7 +88,7 @@ impl TextPipeline { fonts: &Assets, text_spans: impl Iterator, linebreak: LineBreak, - justify: JustifyText, + justify: Justify, bounds: TextBounds, scale_factor: f64, computed: &mut ComputedTextBlock, @@ -197,7 +201,7 @@ impl TextPipeline { // Workaround for alignment not working for unbounded text. // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != JustifyText::Left { + if bounds.width.is_none() && justify != Justify::Left { let dimensions = buffer_dimensions(buffer); // `set_size` causes a re-layout to occur. buffer.set_size(font_system, Some(dimensions.x), bounds.height); @@ -228,7 +232,6 @@ impl TextPipeline { font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, - y_axis_orientation: YAxisOrientation, computed: &mut ComputedTextBlock, font_system: &mut CosmicFontSystem, swash_cache: &mut SwashCache, @@ -337,7 +340,7 @@ impl TextPipeline { ) })?; - let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); + let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap(); let location = atlas_info.location; let glyph_rect = texture_atlas.textures[location.glyph_index]; let left = location.offset.x as f32; @@ -348,10 +351,6 @@ impl TextPipeline { let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; let y = line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; - let y = match y_axis_orientation { - YAxisOrientation::TopToBottom => y, - YAxisOrientation::BottomToTop => box_size.y - y, - }; let position = Vec2::new(x, y); @@ -489,7 +488,8 @@ impl TextMeasureInfo { } } -fn load_font_to_fontdb( +/// Add the font to the cosmic text's `FontSystem`'s in-memory font database +pub fn load_font_to_fontdb( text_font: &TextFont, font_system: &mut cosmic_text::FontSystem, map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, Arc)>, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index e9e78e3ed2..330f0d977a 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,8 +1,3 @@ -pub use cosmic_text::{ - self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, - Weight as FontWeight, -}; - use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; use bevy_color::Color; @@ -121,19 +116,19 @@ impl Default for ComputedTextBlock { pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. - pub justify: JustifyText, + pub justify: Justify, /// How the text should linebreak when running out of the bounds determined by `max_size`. pub linebreak: LineBreak, } impl TextLayout { /// Makes a new [`TextLayout`]. - pub const fn new(justify: JustifyText, linebreak: LineBreak) -> Self { + pub const fn new(justify: Justify, linebreak: LineBreak) -> Self { Self { justify, linebreak } } - /// Makes a new [`TextLayout`] with the specified [`JustifyText`]. - pub fn new_with_justify(justify: JustifyText) -> Self { + /// Makes a new [`TextLayout`] with the specified [`Justify`]. + pub fn new_with_justify(justify: Justify) -> Self { Self::default().with_justify(justify) } @@ -148,8 +143,8 @@ impl TextLayout { Self::default().with_no_wrap() } - /// Returns this [`TextLayout`] with the specified [`JustifyText`]. - pub const fn with_justify(mut self, justify: JustifyText) -> Self { + /// Returns this [`TextLayout`] with the specified [`Justify`]. + pub const fn with_justify(mut self, justify: Justify) -> Self { self.justify = justify; self } @@ -251,7 +246,7 @@ impl From for TextSpan { /// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] -pub enum JustifyText { +pub enum Justify { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] @@ -268,20 +263,20 @@ pub enum JustifyText { Justified, } -impl From for cosmic_text::Align { - fn from(justify: JustifyText) -> Self { +impl From for cosmic_text::Align { + fn from(justify: Justify) -> Self { match justify { - JustifyText::Left => cosmic_text::Align::Left, - JustifyText::Center => cosmic_text::Align::Center, - JustifyText::Right => cosmic_text::Align::Right, - JustifyText::Justified => cosmic_text::Align::Justified, + Justify::Left => cosmic_text::Align::Left, + Justify::Center => cosmic_text::Align::Center, + Justify::Right => cosmic_text::Align::Right, + Justify::Justified => cosmic_text::Align::Justified, } } } /// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically /// the font face, the font size, and the color. -#[derive(Component, Clone, Debug, Reflect)] +#[derive(Component, Clone, Debug, Reflect, PartialEq)] #[reflect(Component, Default, Debug, Clone)] pub struct TextFont { /// The specific font face to use, as a `Handle` to a [`Font`] asset. @@ -359,8 +354,8 @@ impl Default for TextFont { /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size -#[derive(Debug, Clone, Copy, Reflect)] -#[reflect(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Debug, Clone, PartialEq)] pub enum LineHeight { /// Set line height to a specific number of pixels Px(f32), diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index a9419e89c0..7fa3202d18 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -2,7 +2,7 @@ use crate::pipeline::CosmicFontSystem; use crate::{ ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, - TextSpanAccess, TextWriter, YAxisOrientation, + TextSpanAccess, TextWriter, }; use bevy_asset::Assets; use bevy_color::LinearRgba; @@ -51,7 +51,7 @@ use bevy_window::{PrimaryWindow, Window}; /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, Text2d, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, Text2d, TextLayout, TextFont, TextColor, TextSpan}; /// # /// # let font_handle: Handle = Default::default(); /// # let mut world = World::default(); @@ -73,7 +73,7 @@ use bevy_window::{PrimaryWindow, Window}; /// // With text justification. /// world.spawn(( /// Text2d::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans @@ -182,10 +182,10 @@ pub fn extract_text2d_sprite( text_bounds.width.unwrap_or(text_layout_info.size.x), text_bounds.height.unwrap_or(text_layout_info.size.y), ); - let bottom_left = - -(anchor.as_vec() + 0.5) * size + (size.y - text_layout_info.size.y) * Vec2::Y; + + let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; let transform = - *global_transform * GlobalTransform::from_translation(bottom_left.extend(0.)) * scaling; + *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling; let mut color = LinearRgba::WHITE; let mut current_span = usize::MAX; @@ -213,12 +213,12 @@ pub fn extract_text2d_sprite( current_span = *span_index; } let rect = texture_atlases - .get(&atlas_info.texture_atlas) + .get(atlas_info.texture_atlas) .unwrap() .textures[atlas_info.location.glyph_index] .as_rect(); extracted_slices.slices.push(ExtractedSlice { - offset: *position, + offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), }); @@ -232,7 +232,7 @@ pub fn extract_text2d_sprite( render_entity, transform, color, - image_handle_id: atlas_info.texture.id(), + image_handle_id: atlas_info.texture, flip_x: false, flip_y: false, kind: bevy_sprite::ExtractedSpriteKind::Slices { @@ -316,7 +316,6 @@ pub fn update_text2d_layout( &mut font_atlas_sets, &mut texture_atlases, &mut textures, - YAxisOrientation::BottomToTop, computed.as_mut(), &mut font_system, &mut swash_cache, diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 520782b519..7c8e840ace 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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/common_conditions.rs b/crates/bevy_time/src/common_conditions.rs index d944303439..cb0b30d13e 100644 --- a/crates/bevy_time/src/common_conditions.rs +++ b/crates/bevy_time/src/common_conditions.rs @@ -167,7 +167,7 @@ pub fn repeating_after_delay(duration: Duration) -> impl FnMut(Res

for SetGradientViewBindGroup { + type Param = SRes; + type ViewQuery = Read; + type ItemQuery = (); + + fn render<'w>( + _item: &P, + view_uniform: &'w ViewUniformOffset, + _entity: Option<()>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else { + return RenderCommandResult::Failure("view_bind_group not available"); + }; + pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]); + RenderCommandResult::Success + } +} + +pub struct DrawGradient; +impl RenderCommand

for DrawGradient { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: Option<&'w GradientBatch>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(batch) = batch else { + return RenderCommandResult::Skip; + }; + let ui_meta = ui_meta.into_inner(); + let Some(vertices) = ui_meta.vertices.buffer() else { + return RenderCommandResult::Failure("missing vertices to draw ui"); + }; + let Some(indices) = ui_meta.indices.buffer() else { + return RenderCommandResult::Failure("missing indices to draw ui"); + }; + + // Store the vertices + pass.set_vertex_buffer(0, vertices.slice(..)); + // Define how to "connect" the vertices + pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32); + // Draw the vertices + pass.draw_indexed(batch.range.clone(), 0, 0..1); + RenderCommandResult::Success + } +} diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl new file mode 100644 index 0000000000..54bc35eb14 --- /dev/null +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -0,0 +1,449 @@ +#import bevy_render::view::View +#import bevy_ui::ui_node::{ + draw_uinode_background, + draw_uinode_border, +} + +const PI: f32 = 3.14159265358979323846; +const TAU: f32 = 2. * PI; + +const TEXTURED = 1u; +const RIGHT_VERTEX = 2u; +const BOTTOM_VERTEX = 4u; +// must align with BORDER_* shader_flags from bevy_ui/render/mod.rs +const RADIAL: u32 = 16u; +const FILL_START: u32 = 32u; +const FILL_END: u32 = 64u; +const CONIC: u32 = 128u; +const BORDER_LEFT: u32 = 256u; +const BORDER_TOP: u32 = 512u; +const BORDER_RIGHT: u32 = 1024u; +const BORDER_BOTTOM: u32 = 2048u; +const BORDER_ANY: u32 = BORDER_LEFT + BORDER_TOP + BORDER_RIGHT + BORDER_BOTTOM; + +fn enabled(flags: u32, mask: u32) -> bool { + return (flags & mask) != 0u; +} + +@group(0) @binding(0) var view: View; + +struct GradientVertexOutput { + @location(0) uv: vec2, + @location(1) @interpolate(flat) size: vec2, + @location(2) @interpolate(flat) flags: u32, + @location(3) @interpolate(flat) radius: vec4, + @location(4) @interpolate(flat) border: vec4, + + // Position relative to the center of the rectangle. + @location(5) point: vec2, + @location(6) @interpolate(flat) g_start: vec2, + @location(7) @interpolate(flat) dir: vec2, + @location(8) @interpolate(flat) start_color: vec4, + @location(9) @interpolate(flat) start_len: f32, + @location(10) @interpolate(flat) end_len: f32, + @location(11) @interpolate(flat) end_color: vec4, + @location(12) @interpolate(flat) hint: f32, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2, + @location(2) flags: u32, + + // x: top left, y: top right, z: bottom right, w: bottom left. + @location(3) radius: vec4, + + // x: left, y: top, z: right, w: bottom. + @location(4) border: vec4, + @location(5) size: vec2, + @location(6) point: vec2, + @location(7) @interpolate(flat) g_start: vec2, + @location(8) @interpolate(flat) dir: vec2, + @location(9) @interpolate(flat) start_color: vec4, + @location(10) @interpolate(flat) start_len: f32, + @location(11) @interpolate(flat) end_len: f32, + @location(12) @interpolate(flat) end_color: vec4, + @location(13) @interpolate(flat) hint: f32 +) -> GradientVertexOutput { + var out: GradientVertexOutput; + out.position = view.clip_from_world * vec4(vertex_position, 1.0); + out.uv = vertex_uv; + out.size = size; + out.flags = flags; + out.radius = radius; + out.border = border; + out.point = point; + out.dir = dir; + out.start_color = start_color; + out.start_len = start_len; + out.end_len = end_len; + out.end_color = end_color; + out.g_start = g_start; + out.hint = hint; + + return out; +} + +@fragment +fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { + var g_distance: f32; + if enabled(in.flags, RADIAL) { + g_distance = radial_distance(in.point, in.g_start, in.dir.x); + } else if enabled(in.flags, CONIC) { + g_distance = conic_distance(in.dir.x, in.point, in.g_start); + } else { + g_distance = linear_distance(in.point, in.g_start, in.dir); + } + + let gradient_color = interpolate_gradient( + g_distance, + in.start_color, + in.start_len, + in.end_color, + in.end_len, + in.hint, + in.flags + ); + + if enabled(in.flags, BORDER_ANY) { + return draw_uinode_border(gradient_color, in.point, in.size, in.radius, in.border, in.flags); + } else { + return draw_uinode_background(gradient_color, in.point, in.size, in.radius, in.border); + } +} + +// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space. +fn mix_linear_rgba_in_srgba_space(a: vec4, b: vec4, t: f32) -> vec4 { + let a_srgb = pow(a.rgb, vec3(1. / 2.2)); + let b_srgb = pow(b.rgb, vec3(1. / 2.2)); + let mixed_srgb = mix(a_srgb, b_srgb, t); + return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); +} + +fn linear_rgba_to_oklaba(c: vec4) -> 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.a + ); +} + +fn oklaba_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.a + ); +} + +fn mix_linear_rgba_in_oklaba_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t)); +} + +fn linear_rgba_to_hsla(c: vec4) -> vec4 { + let max = max(max(c.r, c.g), c.b); + let min = min(min(c.r, c.g), c.b); + let l = (max + min) * 0.5; + if max == min { + return vec4(0., 0., l, c.a); + } else { + let delta = max - min; + let s = delta / (1. - abs(2. * l - 1.)); + var h = 0.; + if max == c.r { + h = ((c.g - c.b) / delta) % 6.; + } else if max == c.g { + h = ((c.b - c.r) / delta) + 2.; + } else { + h = ((c.r - c.g) / delta) + 4.; + } + h = h / 6.; + return vec4(h, s, l, c.a); + } +} + + +fn hsla_to_linear_rgba(hsl: vec4) -> vec4 { + let h = hsl.x; + let s = hsl.y; + let l = hsl.z; + let c = (1.0 - abs(2.0 * l - 1.0)) * s; + let hp = h * 6.0; + let x = c * (1.0 - abs(hp % 2.0 - 1.0)); + var r: f32 = 0.0; + var g: f32 = 0.0; + var b: f32 = 0.0; + if 0.0 <= hp && hp < 1.0 { + r = c; g = x; b = 0.0; + } else if 1.0 <= hp && hp < 2.0 { + r = x; g = c; b = 0.0; + } else if 2.0 <= hp && hp < 3.0 { + r = 0.0; g = c; b = x; + } else if 3.0 <= hp && hp < 4.0 { + r = 0.0; g = x; b = c; + } else if 4.0 <= hp && hp < 5.0 { + r = x; g = 0.0; b = c; + } else if 5.0 <= hp && hp < 6.0 { + r = c; g = 0.0; b = x; + } + let m = l - 0.5 * c; + return vec4(r + m, g + m, b + m, hsl.a); +} + +fn linear_rgba_to_hsva(c: vec4) -> vec4 { + let maxc = max(max(c.r, c.g), c.b); + let minc = min(min(c.r, c.g), c.b); + let delta = maxc - minc; + var h: f32 = 0.0; + var s: f32 = 0.0; + let v: f32 = maxc; + if delta != 0.0 { + s = delta / maxc; + if maxc == c.r { + h = ((c.g - c.b) / delta) % 6.0; + } else if maxc == c.g { + h = ((c.b - c.r) / delta) + 2.0; + } else { + h = ((c.r - c.g) / delta) + 4.0; + } + h = h / 6.0; + if h < 0.0 { + h = h + 1.0; + } + } + return vec4(h, s, v, c.a); +} + +fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { + let h = hsva.x * 6.0; + let s = hsva.y; + let v = hsva.z; + let c = v * s; + let x = c * (1.0 - abs(h % 2.0 - 1.0)); + let m = v - c; + var r: f32 = 0.0; + var g: f32 = 0.0; + var b: f32 = 0.0; + if 0.0 <= h && h < 1.0 { + r = c; g = x; b = 0.0; + } else if 1.0 <= h && h < 2.0 { + r = x; g = c; b = 0.0; + } else if 2.0 <= h && h < 3.0 { + r = 0.0; g = c; b = x; + } else if 3.0 <= h && h < 4.0 { + r = 0.0; g = x; b = c; + } else if 4.0 <= h && h < 5.0 { + r = x; g = 0.0; b = c; + } else if 5.0 <= h && h < 6.0 { + r = c; g = 0.0; b = x; + } + return vec4(r + m, g + m, b + m, hsva.a); +} + +/// hue is left in radians and not converted to degrees +fn linear_rgba_to_oklcha(c: vec4) -> vec4 { + let o = linear_rgba_to_oklaba(c); + let chroma = sqrt(o.y * o.y + o.z * o.z); + let hue = atan2(o.z, o.y); + return vec4(o.x, chroma, rem_euclid(hue, TAU), o.a); +} + +fn oklcha_to_linear_rgba(c: vec4) -> vec4 { + let a = c.y * cos(c.z); + let b = c.y * sin(c.z); + return oklaba_to_linear_rgba(vec4(c.x, a, b, c.a)); +} + +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 + (diff + select(TAU, -TAU, 0. < diff)) * t, TAU); +} + +fn mix_oklcha(a: vec4, b: vec4, t: f32) -> vec4 { + let ah = select(a.z, b.z, a.y == 0.); + let bh = select(b.z, a.z, b.y == 0.); + return vec4( + mix(a.xy, b.xy, t), + lerp_hue(ah, bh, t), + mix(a.a, b.a, t) + ); +} + +fn mix_oklcha_long(a: vec4, b: vec4, t: f32) -> vec4 { + let ah = select(a.z, b.z, a.y == 0.); + let bh = select(b.z, a.z, b.y == 0.); + return vec4( + mix(a.xy, b.xy, t), + lerp_hue_long(ah, bh, t), + mix(a.w, b.w, t) + ); +} + +fn mix_linear_rgba_in_oklcha_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t)); +} + +fn mix_linear_rgba_in_oklcha_space_long(a: vec4, b: vec4, t: f32) -> vec4 { + return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t)); +} + +fn mix_linear_rgba_in_hsva_space(a: vec4, b: vec4, t: f32) -> vec4 { + let ha = linear_rgba_to_hsva(a); + let hb = linear_rgba_to_hsva(b); + var h: f32; + if ha.y == 0. { + h = hb.x; + } else if hb.y == 0. { + h = ha.x; + } else { + h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU; + } + let s = mix(ha.y, hb.y, t); + let v = mix(ha.z, hb.z, t); + let a_alpha = mix(ha.a, hb.a, t); + return hsva_to_linear_rgba(vec4(h, s, v, a_alpha)); +} + +fn mix_linear_rgba_in_hsva_space_long(a: vec4, b: vec4, t: f32) -> vec4 { + let ha = linear_rgba_to_hsva(a); + let hb = linear_rgba_to_hsva(b); + let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU; + let s = mix(ha.y, hb.y, t); + let v = mix(ha.z, hb.z, t); + let a_alpha = mix(ha.a, hb.a, t); + return hsva_to_linear_rgba(vec4(h, s, v, a_alpha)); +} + +fn mix_linear_rgba_in_hsla_space(a: vec4, b: vec4, t: f32) -> vec4 { + let ha = linear_rgba_to_hsla(a); + let hb = linear_rgba_to_hsla(b); + let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU; + let s = mix(ha.y, hb.y, t); + let l = mix(ha.z, hb.z, t); + let a_alpha = mix(ha.a, hb.a, t); + return hsla_to_linear_rgba(vec4(h, s, l, a_alpha)); +} + +fn mix_linear_rgba_in_hsla_space_long(a: vec4, b: vec4, t: f32) -> vec4 { + let ha = linear_rgba_to_hsla(a); + let hb = linear_rgba_to_hsla(b); + let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU; + let s = mix(ha.y, hb.y, t); + let l = mix(ha.z, hb.z, t); + let a_alpha = mix(ha.a, hb.a, t); + return hsla_to_linear_rgba(vec4(h, s, l, a_alpha)); +} + +// These functions are used to calculate the distance in gradient space from the start of the gradient to the point. +// The distance in gradient space is then used to interpolate between the start and end colors. + +fn linear_distance( + point: vec2, + g_start: vec2, + g_dir: vec2, +) -> f32 { + return dot(point - g_start, g_dir); +} + +fn radial_distance( + point: vec2, + center: vec2, + ratio: f32, +) -> f32 { + let d = point - center; + return length(vec2(d.x, d.y * ratio)); +} + +fn conic_distance( + start: f32, + point: vec2, + center: vec2, +) -> f32 { + let d = point - center; + let angle = atan2(-d.x, d.y) + PI; + return (((angle - start) % TAU) + TAU) % TAU; +} + +fn interpolate_gradient( + distance: f32, + start_color: vec4, + start_distance: f32, + end_color: vec4, + end_distance: f32, + hint: f32, + flags: u32, +) -> vec4 { + if start_distance == end_distance { + if distance <= start_distance && enabled(flags, FILL_START) { + return start_color; + } + if start_distance <= distance && enabled(flags, FILL_END) { + return end_color; + } + return vec4(0.); + } + + var t = (distance - start_distance) / (end_distance - start_distance); + + if t < 0.0 { + if enabled(flags, FILL_START) { + return start_color; + } + return vec4(0.0); + } + + if 1. < t { + if enabled(flags, FILL_END) { + return end_color; + } + return vec4(0.0); + } + + if t < hint { + t = 0.5 * t / hint; + } else { + t = 0.5 * (1 + (t - hint) / (1.0 - hint)); + } + +#ifdef IN_SRGB + return mix_linear_rgba_in_srgba_space(start_color, end_color, t); +#else ifdef IN_OKLAB + return mix_linear_rgba_in_oklaba_space(start_color, end_color, t); +#else ifdef IN_OKLCH + return mix_linear_rgba_in_oklcha_space(start_color, end_color, t); +#else ifdef IN_OKLCH_LONG + return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t); +#else ifdef IN_HSV + return mix_linear_rgba_in_hsva_space(start_color, end_color, t); +#else ifdef IN_HSV_LONG + return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t); +#else ifdef IN_HSL + return mix_linear_rgba_in_hsla_space(start_color, end_color, t); +#else ifdef IN_HSL_LONG + return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t); +#else + return mix(start_color, end_color, t); +#endif +} diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui_render/src/lib.rs similarity index 78% rename from crates/bevy_ui/src/render/mod.rs rename to crates/bevy_ui_render/src/lib.rs index e811cfe362..74617a7269 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1,19 +1,33 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + +//! Provides rendering functionality for `bevy_ui`. + pub mod box_shadow; +mod gradient; mod pipeline; mod render_pass; +pub mod ui_material; mod ui_material_pipeline; pub mod ui_texture_slice_pipeline; #[cfg(feature = "bevy_ui_debug")] mod debug_overlay; -use crate::widget::{ImageNode, ViewportNode}; -use crate::{ - BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, - ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, +use bevy_reflect::prelude::ReflectDefault; +use bevy_reflect::Reflect; +use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode}; +use bevy_ui::{ + BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedNodeTarget, Display, Node, + Outline, ResolvedBorderRadius, UiGlobalTransform, }; + use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetEvent, AssetId, Assets, Handle}; +use bevy_asset::{AssetEvent, AssetId, Assets}; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; @@ -21,13 +35,13 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2}; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::renderer::RenderContext; use bevy_render::sync_world::MainEntity; use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE; -use bevy_render::view::RetainedViewEntity; +use bevy_render::view::{Hdr, InheritedVisibility, RetainedViewEntity}; use bevy_render::{ camera::Camera, render_asset::RenderAssets, @@ -38,18 +52,18 @@ use bevy_render::{ view::{ExtractedView, ViewUniforms}, Extract, RenderApp, RenderSystems, }; +use bevy_render::{load_shader_library, RenderStartup}; use bevy_render::{ render_phase::{PhaseItem, PhaseItemExtraIndex}, sync_world::{RenderEntity, TemporaryRenderEntity}, texture::GpuImage, - view::InheritedVisibility, ExtractSchedule, Render, }; use bevy_sprite::{BorderRect, SpriteAssetEvents}; #[cfg(feature = "bevy_ui_debug")] pub use debug_overlay::UiDebugOptions; +use gradient::GradientPlugin; -use crate::{Display, Node}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_text::{ ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextColor, TextLayoutInfo, @@ -58,6 +72,7 @@ use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; use bytemuck::{Pod, Zeroable}; use core::ops::Range; + use graph::{NodeUi, SubGraphUi}; pub use pipeline::*; pub use render_pass::*; @@ -76,7 +91,16 @@ pub mod graph { } } -/// Z offsets of "extracted nodes" for a given entity. These exist to allow rendering multiple "extracted nodes" +pub mod prelude { + #[cfg(feature = "bevy_ui_debug")] + pub use crate::debug_overlay::UiDebugOptions; + + pub use crate::{ + ui_material::*, ui_material_pipeline::UiMaterialPlugin, BoxShadowSamples, UiAntiAlias, + }; +} + +/// Local Z offsets of "extracted nodes" for a given entity. These exist to allow rendering multiple "extracted nodes" /// for a given source entity (ex: render both a background color _and_ a custom material for a given node). /// /// When possible these offsets should be defined in _this_ module to ensure z-index coordination across contexts. @@ -92,13 +116,15 @@ pub mod graph { /// a positive offset on a node below. pub mod stack_z_offsets { pub const BOX_SHADOW: f32 = -0.1; - pub const TEXTURE_SLICE: f32 = 0.0; - pub const NODE: f32 = 0.0; - pub const MATERIAL: f32 = 0.18267; + pub const BACKGROUND_COLOR: f32 = 0.0; + pub const BORDER: f32 = 0.01; + pub const GRADIENT: f32 = 0.02; + pub const BORDER_GRADIENT: f32 = 0.03; + pub const IMAGE: f32 = 0.04; + pub const MATERIAL: f32 = 0.05; + pub const TEXT: f32 = 0.06; } -pub const UI_SHADER_HANDLE: Handle = weak_handle!("7d190d05-545b-42f5-bd85-22a0da85b0f6"); - #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderUiSystems { ExtractCameraViews, @@ -112,91 +138,161 @@ pub enum RenderUiSystems { ExtractTextShadows, ExtractText, ExtractDebug, + ExtractGradient, +} + +/// Marker for controlling whether UI is rendered with or without anti-aliasing +/// in a camera. By default, UI is always anti-aliased. +/// +/// **Note:** This does not affect text anti-aliasing. For that, use the `font_smoothing` property of the [`TextFont`](bevy_text::TextFont) component. +/// +/// ``` +/// use bevy_core_pipeline::prelude::*; +/// use bevy_ecs::prelude::*; +/// use bevy_ui::prelude::*; +/// use bevy_ui_render::prelude::*; +/// +/// fn spawn_camera(mut commands: Commands) { +/// commands.spawn(( +/// Camera2d, +/// // This will cause all UI in this camera to be rendered without +/// // anti-aliasing +/// UiAntiAlias::Off, +/// )); +/// } +/// ``` +#[derive(Component, Clone, Copy, Default, Debug, Reflect, Eq, PartialEq)] +#[reflect(Component, Default, PartialEq, Clone)] +pub enum UiAntiAlias { + /// UI will render with anti-aliasing + #[default] + On, + /// UI will render without anti-aliasing + Off, +} + +/// Number of shadow samples. +/// A larger value will result in higher quality shadows. +/// Default is 4, values higher than ~10 offer diminishing returns. +/// +/// ``` +/// use bevy_core_pipeline::prelude::*; +/// use bevy_ecs::prelude::*; +/// use bevy_ui::prelude::*; +/// use bevy_ui_render::prelude::*; +/// +/// fn spawn_camera(mut commands: Commands) { +/// commands.spawn(( +/// Camera2d, +/// BoxShadowSamples(6), +/// )); +/// } +/// ``` +#[derive(Component, Clone, Copy, Debug, Reflect, Eq, PartialEq)] +#[reflect(Component, Default, PartialEq, Clone)] +pub struct BoxShadowSamples(pub u32); + +impl Default for BoxShadowSamples { + fn default() -> Self { + Self(4) + } } /// Deprecated alias for [`RenderUiSystems`]. #[deprecated(since = "0.17.0", note = "Renamed to `RenderUiSystems`.")] pub type RenderUiSystem = RenderUiSystems; -pub fn build_ui_render(app: &mut App) { - load_internal_asset!(app, UI_SHADER_HANDLE, "ui.wgsl", Shader::from_wgsl); +#[derive(Default)] +pub struct UiRenderPlugin; - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; +impl Plugin for UiRenderPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "ui.wgsl"); + app.register_type::() + .register_type::(); - render_app - .init_resource::>() - .init_resource::() - .init_resource::() - .init_resource::() - .allow_ambiguous_resource::() - .init_resource::>() - .init_resource::>() - .add_render_command::() - .configure_sets( - ExtractSchedule, - ( - RenderUiSystems::ExtractCameraViews, - RenderUiSystems::ExtractBoxShadows, - RenderUiSystems::ExtractBackgrounds, - RenderUiSystems::ExtractImages, - RenderUiSystems::ExtractTextureSlice, - RenderUiSystems::ExtractBorders, - RenderUiSystems::ExtractTextBackgrounds, - RenderUiSystems::ExtractTextShadows, - RenderUiSystems::ExtractText, - RenderUiSystems::ExtractDebug, + #[cfg(feature = "bevy_ui_debug")] + app.init_resource::(); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .allow_ambiguous_resource::() + .init_resource::>() + .init_resource::>() + .add_render_command::() + .configure_sets( + ExtractSchedule, + ( + RenderUiSystems::ExtractCameraViews, + RenderUiSystems::ExtractBoxShadows, + RenderUiSystems::ExtractBackgrounds, + RenderUiSystems::ExtractImages, + RenderUiSystems::ExtractTextureSlice, + RenderUiSystems::ExtractBorders, + RenderUiSystems::ExtractTextBackgrounds, + RenderUiSystems::ExtractTextShadows, + RenderUiSystems::ExtractText, + RenderUiSystems::ExtractDebug, + ) + .chain(), ) - .chain(), - ) - .add_systems( - ExtractSchedule, - ( - extract_ui_camera_view.in_set(RenderUiSystems::ExtractCameraViews), - extract_uinode_background_colors.in_set(RenderUiSystems::ExtractBackgrounds), - extract_uinode_images.in_set(RenderUiSystems::ExtractImages), - extract_uinode_borders.in_set(RenderUiSystems::ExtractBorders), - extract_viewport_nodes.in_set(RenderUiSystems::ExtractViewportNodes), - extract_text_background_colors.in_set(RenderUiSystems::ExtractTextBackgrounds), - extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), - extract_text_sections.in_set(RenderUiSystems::ExtractText), - #[cfg(feature = "bevy_ui_debug")] - debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), - ), - ) - .add_systems( - Render, - ( - queue_uinodes.in_set(RenderSystems::Queue), - sort_phase_system::.in_set(RenderSystems::PhaseSort), - prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), - ), - ); + .add_systems(RenderStartup, init_ui_pipeline) + .add_systems( + ExtractSchedule, + ( + extract_ui_camera_view.in_set(RenderUiSystems::ExtractCameraViews), + extract_uinode_background_colors.in_set(RenderUiSystems::ExtractBackgrounds), + extract_uinode_images.in_set(RenderUiSystems::ExtractImages), + extract_uinode_borders.in_set(RenderUiSystems::ExtractBorders), + extract_viewport_nodes.in_set(RenderUiSystems::ExtractViewportNodes), + extract_text_background_colors.in_set(RenderUiSystems::ExtractTextBackgrounds), + extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), + extract_text_sections.in_set(RenderUiSystems::ExtractText), + #[cfg(feature = "bevy_ui_debug")] + debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), + ), + ) + .add_systems( + Render, + ( + queue_uinodes.in_set(RenderSystems::Queue), + sort_phase_system::.in_set(RenderSystems::PhaseSort), + prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), + ), + ); - // Render graph - let ui_graph_2d = get_ui_graph(render_app); - let ui_graph_3d = get_ui_graph(render_app); - let mut graph = render_app.world_mut().resource_mut::(); + // Render graph + let ui_graph_2d = get_ui_graph(render_app); + let ui_graph_3d = get_ui_graph(render_app); + let mut graph = render_app.world_mut().resource_mut::(); - if let Some(graph_2d) = graph.get_sub_graph_mut(Core2d) { - graph_2d.add_sub_graph(SubGraphUi, ui_graph_2d); - graph_2d.add_node(NodeUi::UiPass, RunUiSubgraphOnUiViewNode); - graph_2d.add_node_edge(Node2d::EndMainPass, NodeUi::UiPass); - graph_2d.add_node_edge(Node2d::EndMainPassPostProcessing, NodeUi::UiPass); - graph_2d.add_node_edge(NodeUi::UiPass, Node2d::Upscaling); + if let Some(graph_2d) = graph.get_sub_graph_mut(Core2d) { + graph_2d.add_sub_graph(SubGraphUi, ui_graph_2d); + graph_2d.add_node(NodeUi::UiPass, RunUiSubgraphOnUiViewNode); + graph_2d.add_node_edge(Node2d::EndMainPass, NodeUi::UiPass); + graph_2d.add_node_edge(Node2d::EndMainPassPostProcessing, NodeUi::UiPass); + graph_2d.add_node_edge(NodeUi::UiPass, Node2d::Upscaling); + } + + if let Some(graph_3d) = graph.get_sub_graph_mut(Core3d) { + graph_3d.add_sub_graph(SubGraphUi, ui_graph_3d); + graph_3d.add_node(NodeUi::UiPass, RunUiSubgraphOnUiViewNode); + graph_3d.add_node_edge(Node3d::EndMainPass, NodeUi::UiPass); + graph_3d.add_node_edge(Node3d::EndMainPassPostProcessing, NodeUi::UiPass); + graph_3d.add_node_edge(NodeUi::UiPass, Node3d::Upscaling); + } + + app.add_plugins(UiTextureSlicerPlugin); + app.add_plugins(GradientPlugin); + app.add_plugins(BoxShadowPlugin); } - - if let Some(graph_3d) = graph.get_sub_graph_mut(Core3d) { - graph_3d.add_sub_graph(SubGraphUi, ui_graph_3d); - graph_3d.add_node(NodeUi::UiPass, RunUiSubgraphOnUiViewNode); - graph_3d.add_node_edge(Node3d::EndMainPass, NodeUi::UiPass); - graph_3d.add_node_edge(Node3d::EndMainPassPostProcessing, NodeUi::UiPass); - graph_3d.add_node_edge(NodeUi::UiPass, Node3d::Upscaling); - } - - app.add_plugins(UiTextureSlicerPlugin); - app.add_plugins(BoxShadowPlugin); } fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { @@ -206,66 +302,6 @@ fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { ui_graph } -pub struct ExtractedUiNode { - pub stack_index: u32, - pub color: LinearRgba, - pub rect: Rect, - pub image: AssetId, - pub clip: Option, - /// Render world entity of the extracted camera corresponding to this node's target camera. - pub extracted_camera_entity: Entity, - pub item: ExtractedUiItem, - pub main_entity: MainEntity, - pub render_entity: Entity, -} - -/// The type of UI node. -/// This is used to determine how to render the UI node. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum NodeType { - Rect, - Border, -} - -pub enum ExtractedUiItem { - Node { - atlas_scaling: Option, - flip_x: bool, - flip_y: bool, - /// Border radius of the UI node. - /// Ordering: top left, top right, bottom right, bottom left. - border_radius: ResolvedBorderRadius, - /// Border thickness of the UI node. - /// Ordering: left, top, right, bottom. - border: BorderRect, - node_type: NodeType, - transform: Mat4, - }, - /// A contiguous sequence of text glyphs from the same section - Glyphs { - /// Indices into [`ExtractedUiNodes::glyphs`] - range: Range, - }, -} - -pub struct ExtractedGlyph { - pub transform: Mat4, - pub rect: Rect, -} - -#[derive(Resource, Default)] -pub struct ExtractedUiNodes { - pub uinodes: Vec, - pub glyphs: Vec, -} - -impl ExtractedUiNodes { - pub fn clear(&mut self) { - self.uinodes.clear(); - self.glyphs.clear(); - } -} - #[derive(SystemParam)] pub struct UiCameraMap<'w, 's> { mapping: Query<'w, 's, RenderEntity>, @@ -291,11 +327,9 @@ pub struct UiCameraMapper<'w, 's> { impl<'w, 's> UiCameraMapper<'w, 's> { /// Returns the render entity corresponding to the given `UiTargetCamera` or the default camera if `None`. pub fn map(&mut self, computed_target: &ComputedNodeTarget) -> Option { - let camera_entity = computed_target.camera; + let camera_entity = computed_target.camera()?; if self.camera_entity != camera_entity { - let Ok(new_render_camera_entity) = self.mapping.get(camera_entity) else { - return None; - }; + let new_render_camera_entity = self.mapping.get(camera_entity).ok()?; self.render_entity = new_render_camera_entity; self.camera_entity = camera_entity; } @@ -308,6 +342,66 @@ impl<'w, 's> UiCameraMapper<'w, 's> { } } +pub struct ExtractedUiNode { + pub z_order: f32, + pub color: LinearRgba, + pub rect: Rect, + pub image: AssetId, + pub clip: Option, + /// Render world entity of the extracted camera corresponding to this node's target camera. + pub extracted_camera_entity: Entity, + pub item: ExtractedUiItem, + pub main_entity: MainEntity, + pub render_entity: Entity, +} + +/// The type of UI node. +/// This is used to determine how to render the UI node. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NodeType { + Rect, + Border(u32), // shader flags +} + +pub enum ExtractedUiItem { + Node { + atlas_scaling: Option, + flip_x: bool, + flip_y: bool, + /// Border radius of the UI node. + /// Ordering: top left, top right, bottom right, bottom left. + border_radius: ResolvedBorderRadius, + /// Border thickness of the UI node. + /// Ordering: left, top, right, bottom. + border: BorderRect, + node_type: NodeType, + transform: Affine2, + }, + /// A contiguous sequence of text glyphs from the same section + Glyphs { + /// Indices into [`ExtractedUiNodes::glyphs`] + range: Range, + }, +} + +pub struct ExtractedGlyph { + pub transform: Affine2, + pub rect: Rect, +} + +#[derive(Resource, Default)] +pub struct ExtractedUiNodes { + pub uinodes: Vec, + pub glyphs: Vec, +} + +impl ExtractedUiNodes { + pub fn clear(&mut self) { + self.uinodes.clear(); + self.glyphs.clear(); + } +} + /// A [`RenderGraphNode`] that executes the UI rendering subgraph on the UI /// view. struct RunUiSubgraphOnUiViewNode; @@ -340,7 +434,7 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -368,7 +462,7 @@ pub fn extract_uinode_background_colors( extracted_uinodes.uinodes.push(ExtractedUiNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, + z_order: uinode.stack_index as f32 + stack_z_offsets::BACKGROUND_COLOR, color: background_color.0.into(), rect: Rect { min: Vec2::ZERO, @@ -379,7 +473,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -399,7 +493,7 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -454,8 +548,8 @@ pub fn extract_uinode_images( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::IMAGE, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: image.color.into(), rect, clip: clip.map(|clip| clip.clip), @@ -463,7 +557,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -483,7 +577,7 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -499,7 +593,7 @@ pub fn extract_uinode_borders( entity, node, computed_node, - global_transform, + transform, inherited_visibility, maybe_clip, camera, @@ -517,30 +611,63 @@ pub fn extract_uinode_borders( // Don't extract borders with zero width along all edges if computed_node.border() != BorderRect::ZERO { - if let Some(border_color) = maybe_border_color.filter(|bc| !bc.0.is_fully_transparent()) - { - extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index: computed_node.stack_index, - color: border_color.0.into(), - rect: Rect { - max: computed_node.size(), - ..Default::default() - }, - image, - clip: maybe_clip.map(|clip| clip.clip), - extracted_camera_entity, - item: ExtractedUiItem::Node { - atlas_scaling: None, - transform: global_transform.compute_matrix(), - flip_x: false, - flip_y: false, - border: computed_node.border(), - border_radius: computed_node.border_radius(), - node_type: NodeType::Border, - }, - main_entity: entity.into(), - render_entity: commands.spawn(TemporaryRenderEntity).id(), - }); + if let Some(border_color) = maybe_border_color { + let border_colors = [ + border_color.left.to_linear(), + border_color.top.to_linear(), + border_color.right.to_linear(), + border_color.bottom.to_linear(), + ]; + + const BORDER_FLAGS: [u32; 4] = [ + shader_flags::BORDER_LEFT, + shader_flags::BORDER_TOP, + shader_flags::BORDER_RIGHT, + shader_flags::BORDER_BOTTOM, + ]; + let mut completed_flags = 0; + + for (i, &color) in border_colors.iter().enumerate() { + if color.is_fully_transparent() { + continue; + } + + let mut border_flags = BORDER_FLAGS[i]; + + if completed_flags & border_flags != 0 { + continue; + } + + for j in i + 1..4 { + if color == border_colors[j] { + border_flags |= BORDER_FLAGS[j]; + } + } + completed_flags |= border_flags; + + extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: computed_node.stack_index as f32 + stack_z_offsets::BORDER, + color, + rect: Rect { + max: computed_node.size(), + ..Default::default() + }, + image, + clip: maybe_clip.map(|clip| clip.clip), + extracted_camera_entity, + item: ExtractedUiItem::Node { + atlas_scaling: None, + transform: transform.into(), + flip_x: false, + flip_y: false, + border: computed_node.border(), + border_radius: computed_node.border_radius(), + node_type: NodeType::Border(border_flags), + }, + main_entity: entity.into(), + render_entity: commands.spawn(TemporaryRenderEntity).id(), + }); + } } } @@ -552,8 +679,8 @@ pub fn extract_uinode_borders( { let outline_size = computed_node.outlined_node_size(); extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: computed_node.stack_index as f32 + stack_z_offsets::BORDER, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: computed_node.stack_index, color: outline.color.into(), rect: Rect { max: outline_size, @@ -563,13 +690,13 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: global_transform.compute_matrix(), + transform: transform.into(), atlas_scaling: None, flip_x: false, flip_y: false, border: BorderRect::all(computed_node.outline_width()), border_radius: computed_node.outline_radius(), - node_type: NodeType::Border, + node_type: NodeType::Border(shader_flags::BORDER_ALL), }, main_entity: entity.into(), }); @@ -624,6 +751,7 @@ pub fn extract_ui_camera_view( Entity, RenderEntity, &Camera, + Has, Option<&UiAntiAlias>, Option<&BoxShadowSamples>, ), @@ -634,7 +762,7 @@ pub fn extract_ui_camera_view( ) { live_entities.clear(); - for (main_entity, render_entity, camera, ui_anti_alias, shadow_samples) in &query { + for (main_entity, render_entity, camera, hdr, ui_anti_alias, shadow_samples) in &query { // ignore inactive cameras if !camera.is_active { commands @@ -670,7 +798,7 @@ pub fn extract_ui_camera_view( UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, ), clip_from_world: None, - hdr: camera.hdr, + hdr, viewport: UVec4::from(( physical_viewport_rect.min, physical_viewport_rect.size(), @@ -711,7 +839,7 @@ pub fn extract_viewport_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -742,8 +870,8 @@ pub fn extract_viewport_nodes( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::IMAGE, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: LinearRgba::WHITE, rect: Rect { min: Vec2::ZERO, @@ -754,7 +882,7 @@ pub fn extract_viewport_nodes( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -774,7 +902,7 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -792,7 +920,7 @@ pub fn extract_text_sections( for ( entity, uinode, - global_transform, + transform, inherited_visibility, clip, camera, @@ -809,8 +937,7 @@ pub fn extract_text_sections( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -823,12 +950,12 @@ pub fn extract_text_sections( ) in text_layout_info.glyphs.iter().enumerate() { let rect = texture_atlases - .get(&atlas_info.texture_atlas) + .get(atlas_info.texture_atlas) .unwrap() .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -846,10 +973,10 @@ pub fn extract_text_sections( .map(|text_color| LinearRgba::from(text_color.0)) .unwrap_or_default(); extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color, - image: atlas_info.texture.id(), + image: atlas_info.texture, clip: clip.map(|clip| clip.clip), extracted_camera_entity, rect, @@ -872,8 +999,8 @@ pub fn extract_text_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &ComputedNodeTarget, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &TextLayoutInfo, @@ -886,16 +1013,8 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - target, - global_transform, - inherited_visibility, - clip, - text_layout_info, - shadow, - ) in &uinode_query + for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -906,9 +1025,9 @@ pub fn extract_text_shadows( continue; }; - let transform = global_transform.affine() - * Mat4::from_translation( - (-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), + let node_transform = Affine2::from(*transform) + * Affine2::from_translation( + -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); for ( @@ -922,12 +1041,12 @@ pub fn extract_text_shadows( ) in text_layout_info.glyphs.iter().enumerate() { let rect = texture_atlases - .get(&atlas_info.texture_atlas) + .get(atlas_info.texture_atlas) .unwrap() .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: node_transform * Affine2::from_translation(*position), rect, }); @@ -935,10 +1054,10 @@ pub fn extract_text_shadows( info.span_index != *span_index || info.atlas_info.texture != atlas_info.texture }) { extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: shadow.color.into(), - image: atlas_info.texture.id(), + image: atlas_info.texture, clip: clip.map(|clip| clip.clip), extracted_camera_entity, rect, @@ -960,7 +1079,7 @@ pub fn extract_text_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -983,8 +1102,8 @@ pub fn extract_text_background_colors( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); for &(section_entity, rect) in text_layout_info.section_rects.iter() { let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { @@ -992,8 +1111,8 @@ pub fn extract_text_background_colors( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: text_background_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, @@ -1004,7 +1123,7 @@ pub fn extract_text_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform * Mat4::from_translation(rect.center().extend(0.)), + transform: transform * Affine2::from_translation(rect.center()), flip_x: false, flip_y: false, border: uinode.border(), @@ -1055,11 +1174,11 @@ impl Default for UiMeta { } } -pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - Vec3::new(0.5, 0.5, 0.0), - Vec3::new(-0.5, 0.5, 0.0), +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + Vec2::new(-0.5, -0.5), + Vec2::new(0.5, -0.5), + Vec2::new(0.5, 0.5), + Vec2::new(-0.5, 0.5), ]; pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; @@ -1072,11 +1191,21 @@ pub struct UiBatch { /// The values here should match the values for the constants in `ui.wgsl` pub mod shader_flags { + /// Texture should be ignored pub const UNTEXTURED: u32 = 0; + /// Textured pub const TEXTURED: u32 = 1; /// Ordering: top left, top right, bottom right, bottom left. pub const CORNERS: [u32; 4] = [0, 2, 2 | 4, 4]; - pub const BORDER: u32 = 8; + pub const RADIAL: u32 = 16; + pub const FILL_START: u32 = 32; + pub const FILL_END: u32 = 64; + pub const CONIC: u32 = 128; + pub const BORDER_LEFT: u32 = 256; + pub const BORDER_TOP: u32 = 512; + pub const BORDER_RIGHT: u32 = 1024; + pub const BORDER_BOTTOM: u32 = 2048; + pub const BORDER_ALL: u32 = BORDER_LEFT + BORDER_TOP + BORDER_RIGHT + BORDER_BOTTOM; } pub fn queue_uinodes( @@ -1128,7 +1257,7 @@ pub fn queue_uinodes( draw_function, pipeline, entity: (extracted_uinode.render_entity, extracted_uinode.main_entity), - sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::NODE), + sort_key: FloatOrd(extracted_uinode.z_order), index, // batch_range will be calculated in prepare_uinodes batch_range: 0..0, @@ -1275,12 +1404,12 @@ pub fn prepare_uinodes( let mut uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -1321,7 +1450,7 @@ pub fn prepare_uinodes( points[3] + positions_diff[3], ]; - let transformed_rect_size = transform.transform_vector3(rect_size); + let transformed_rect_size = transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -1385,8 +1514,8 @@ pub fn prepare_uinodes( }; let color = extracted_uinode.color.to_f32_array(); - if *node_type == NodeType::Border { - flags |= shader_flags::BORDER; + if let NodeType::Border(border_flags) = *node_type { + flags |= border_flags; } for i in 0..4 { @@ -1395,14 +1524,9 @@ pub fn prepare_uinodes( uv: uvs[i].into(), color, flags: flags | shader_flags::CORNERS[i], - radius: [ - border_radius.top_left, - border_radius.top_right, - border_radius.bottom_right, - border_radius.bottom_left, - ], + radius: (*border_radius).into(), border: [border.left, border.top, border.right, border.bottom], - size: rect_size.xy().into(), + size: rect_size.into(), point: points[i].into(), }); } @@ -1424,13 +1548,14 @@ pub fn prepare_uinodes( let color = extracted_uinode.color.to_f32_array(); for glyph in &extracted_uinodes.glyphs[range.clone()] { let glyph_rect = glyph.rect; - let size = glyph.rect.size(); - - let rect_size = glyph_rect.size().extend(1.0); + let rect_size = glyph_rect.size(); // Specify the corners of the glyph let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (glyph.transform * (pos * rect_size).extend(1.)).xyz() + glyph + .transform + .transform_point2(pos * glyph_rect.size()) + .extend(0.) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -1465,7 +1590,7 @@ pub fn prepare_uinodes( // cull nodes that are completely clipped let transformed_rect_size = - glyph.transform.transform_vector3(rect_size); + glyph.transform.transform_vector2(rect_size); if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x.abs() || positions_diff[1].y - positions_diff[2].y @@ -1502,7 +1627,7 @@ pub fn prepare_uinodes( flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], radius: [0.0; 4], border: [0.0; 4], - size: size.into(), + size: rect_size.into(), point: [0.0; 2], }); } diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui_render/src/pipeline.rs similarity index 57% rename from crates/bevy_ui/src/render/pipeline.rs rename to crates/bevy_ui_render/src/pipeline.rs index dd465515c5..6315091271 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui_render/src/pipeline.rs @@ -1,3 +1,4 @@ +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; use bevy_ecs::prelude::*; use bevy_image::BevyDefault as _; use bevy_render::{ @@ -8,41 +9,44 @@ use bevy_render::{ renderer::RenderDevice, view::{ViewTarget, ViewUniform}, }; +use bevy_utils::default; #[derive(Resource)] pub struct UiPipeline { pub view_layout: BindGroupLayout, pub image_layout: BindGroupLayout, + pub shader: Handle, } -impl FromWorld for UiPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); +pub fn init_ui_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let view_layout = render_device.create_bind_group_layout( + "ui_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); - let view_layout = render_device.create_bind_group_layout( - "ui_view_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - uniform_buffer::(true), + let image_layout = render_device.create_bind_group_layout( + "ui_image_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), ), - ); + ), + ); - let image_layout = render_device.create_bind_group_layout( - "ui_image_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: true }), - sampler(SamplerBindingType::Filtering), - ), - ), - ); - - UiPipeline { - view_layout, - image_layout, - } - } + commands.insert_resource(UiPipeline { + view_layout, + image_layout, + shader: load_embedded_asset!(asset_server.as_ref(), "ui.wgsl"), + }); } #[derive(Clone, Copy, Hash, PartialEq, Eq)] @@ -84,15 +88,14 @@ impl SpecializedRenderPipeline for UiPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: super::UI_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.shader.clone(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { - shader: super::UI_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -102,26 +105,11 @@ impl SpecializedRenderPipeline for UiPipeline { blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.image_layout.clone()], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - }, - depth_stencil: None, - multisample: MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, label: Some("ui_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui_render/src/render_pass.rs similarity index 99% rename from crates/bevy_ui/src/render/render_pass.rs rename to crates/bevy_ui_render/src/render_pass.rs index e0b3b20fab..59cbd8e4da 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui_render/src/render_pass.rs @@ -1,6 +1,7 @@ use core::ops::Range; use super::{ImageNodeBindGroups, UiBatch, UiMeta, UiViewTarget}; + use crate::UiCameraView; use bevy_ecs::{ prelude::*, diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui_render/src/ui.wgsl similarity index 75% rename from crates/bevy_ui/src/render/ui.wgsl rename to crates/bevy_ui_render/src/ui.wgsl index 3fd339405d..e7c7ec4350 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui_render/src/ui.wgsl @@ -1,9 +1,16 @@ +#define_import_path bevy_ui::ui_node + #import bevy_render::view::View const TEXTURED = 1u; const RIGHT_VERTEX = 2u; const BOTTOM_VERTEX = 4u; -const BORDER: u32 = 8u; +// must align with BORDER_* shader_flags from bevy_ui/render/mod.rs +const BORDER_LEFT: u32 = 256u; +const BORDER_TOP: u32 = 512u; +const BORDER_RIGHT: u32 = 1024u; +const BORDER_BOTTOM: u32 = 2048u; +const BORDER_ANY: u32 = BORDER_LEFT + BORDER_TOP + BORDER_RIGHT + BORDER_BOTTOM; fn enabled(flags: u32, mask: u32) -> bool { return (flags & mask) != 0u; @@ -114,35 +121,62 @@ fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, in return sd_rounded_box(inner_point, inner_size, r); } +fn nearest_border_active(point_vs_mid: vec2, size: vec2, width: vec4, flags: u32) -> bool { + if (flags & BORDER_ANY) == BORDER_ANY { + return true; + } + + // get point vs top left + let point = clamp(point_vs_mid + size * 0.49999, vec2(0.0), size); + + let left = point.x / width.x; + let top = point.y / width.y; + let right = (size.x - point.x) / width.z; + let bottom = (size.y - point.y) / width.w; + + let min_dist = min(min(left, top), min(right, bottom)); + + return (enabled(flags, BORDER_LEFT) && min_dist == left) || + (enabled(flags, BORDER_TOP) && min_dist == top) || + (enabled(flags, BORDER_RIGHT) && min_dist == right) || + (enabled(flags, BORDER_BOTTOM) && min_dist == bottom); +} + // get alpha for antialiasing for sdf fn antialias(distance: f32) -> f32 { // Using the fwidth(distance) was causing artifacts, so just use the distance. return saturate(0.5 - distance); } -fn draw(in: VertexOutput, texture_color: vec4) -> vec4 { - // Only use the color sampled from the texture if the `TEXTURED` flag is enabled. - // This allows us to draw both textured and untextured shapes together in the same batch. - let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); - +fn draw_uinode_border( + color: vec4, + point: vec2, + size: vec2, + radius: vec4, + border: vec4, + flags: u32, +) -> vec4 { // Signed distances. The magnitude is the distance of the point from the edge of the shape. // * Negative values indicate that the point is inside the shape. // * Zero values indicate the point is on the edge of the shape. // * Positive values indicate the point is outside the shape. // Signed distance from the exterior boundary. - let external_distance = sd_rounded_box(in.point, in.size, in.radius); + let external_distance = sd_rounded_box(point, size, radius); // Signed distance from the border's internal edge (the signed distance is negative if the point // is inside the rect but not on the border). // If the border size is set to zero, this is the same as the external distance. - let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + let internal_distance = sd_inset_rounded_box(point, size, radius, border); // Signed distance from the border (the intersection of the rect with its border). // Points inside the border have negative signed distance. Any point outside the border, whether // outside the outside edge, or inside the inner edge have positive signed distance. let border_distance = max(external_distance, -internal_distance); + // check if this node should apply color for the nearest border + let nearest_border = select(0.0, 1.0, nearest_border_active(point, size, border, flags)); + #ifdef ANTI_ALIAS // At external edges with no border, `border_distance` is equal to zero. // This select statement ensures we only perform anti-aliasing where a non-zero width border @@ -154,14 +188,18 @@ fn draw(in: VertexOutput, texture_color: vec4) -> vec4 { #endif // Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here. - return vec4(color.rgb, saturate(color.a * t)); + return vec4(color.rgb, saturate(color.a * t * nearest_border)); } -fn draw_background(in: VertexOutput, texture_color: vec4) -> vec4 { - let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); - +fn draw_uinode_background( + color: vec4, + point: vec2, + size: vec2, + radius: vec4, + border: vec4, +) -> vec4 { // When drawing the background only draw the internal area and not the border. - let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + let internal_distance = sd_inset_rounded_box(point, size, radius, border); #ifdef ANTI_ALIAS let t = antialias(internal_distance); @@ -176,9 +214,13 @@ fn draw_background(in: VertexOutput, texture_color: vec4) -> vec4 { fn fragment(in: VertexOutput) -> @location(0) vec4 { let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); - if enabled(in.flags, BORDER) { - return draw(in, texture_color); + // Only use the color sampled from the texture if the `TEXTURED` flag is enabled. + // This allows us to draw both textured and untextured shapes together in the same batch. + let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); + + if enabled(in.flags, BORDER_ANY) { + return draw_uinode_border(color, in.point, in.size, in.radius, in.border, in.flags); } else { - return draw_background(in, texture_color); + return draw_uinode_background(color, in.point, in.size, in.radius, in.border); } } diff --git a/crates/bevy_ui_render/src/ui_material.rs b/crates/bevy_ui_render/src/ui_material.rs new file mode 100644 index 0000000000..8208077544 --- /dev/null +++ b/crates/bevy_ui_render/src/ui_material.rs @@ -0,0 +1,182 @@ +use crate::Node; +use bevy_asset::{Asset, AssetId, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::{ + extract_component::ExtractComponent, + render_resource::{AsBindGroup, RenderPipelineDescriptor, ShaderRef}, +}; +use derive_more::derive::From; + +/// Materials are used alongside [`UiMaterialPlugin`](crate::UiMaterialPlugin) and [`MaterialNode`] +/// to spawn entities that are rendered with a specific [`UiMaterial`] type. They serve as an easy to use high level +/// way to render `Node` entities with custom shader logic. +/// +/// `UiMaterials` must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. +/// +/// Materials must also implement [`Asset`] so they can be treated as such. +/// +/// If you are only using the fragment shader, make sure your shader imports the `UiVertexOutput` +/// from `bevy_ui::ui_vertex_output` and uses it as the input of your fragment shader like the +/// example below does. +/// +/// # Example +/// +/// Here is a simple [`UiMaterial`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// check out the [`AsBindGroup`] documentation. +/// ``` +/// # use bevy_ui::prelude::*; +/// # use bevy_ecs::prelude::*; +/// # use bevy_image::Image; +/// # use bevy_reflect::TypePath; +/// # use bevy_render::render_resource::{AsBindGroup, ShaderRef}; +/// # use bevy_color::LinearRgba; +/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// # use bevy_ui_render::prelude::*; +/// +/// #[derive(AsBindGroup, Asset, TypePath, Debug, Clone)] +/// pub struct CustomMaterial { +/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to +/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. +/// #[uniform(0)] +/// color: LinearRgba, +/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just +/// // add the sampler attribute with a different binding index. +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// +/// // All functions on `UiMaterial` have default impls. You only need to implement the +/// // functions that are relevant for your material. +/// impl UiMaterial for CustomMaterial { +/// fn fragment_shader() -> ShaderRef { +/// "shaders/custom_material.wgsl".into() +/// } +/// } +/// +/// // Spawn an entity using `CustomMaterial`. +/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { +/// commands.spawn(( +/// MaterialNode(materials.add(CustomMaterial { +/// color: LinearRgba::RED, +/// color_texture: asset_server.load("some_image.png"), +/// })), +/// Node { +/// width: Val::Percent(100.0), +/// ..Default::default() +/// }, +/// )); +/// } +/// ``` +/// In WGSL shaders, the material's binding would look like this: +/// +/// If you only use the fragment shader make sure to import `UiVertexOutput` from +/// `bevy_ui::ui_vertex_output` in your wgsl shader. +/// Also note that bind group 0 is always bound to the [`View Uniform`](bevy_render::view::ViewUniform) +/// and the [`Globals Uniform`](bevy_render::globals::GlobalsUniform). +/// +/// ```wgsl +/// #import bevy_ui::ui_vertex_output UiVertexOutput +/// +/// struct CustomMaterial { +/// color: vec4, +/// } +/// +/// @group(1) @binding(0) +/// var material: CustomMaterial; +/// @group(1) @binding(1) +/// var color_texture: texture_2d; +/// @group(1) @binding(2) +/// var color_sampler: sampler; +/// +/// @fragment +/// fn fragment(in: UiVertexOutput) -> @location(0) vec4 { +/// +/// } +/// ``` +pub trait UiMaterial: AsBindGroup + Asset + Clone + Sized { + /// Returns this materials vertex shader. If [`ShaderRef::Default`] is returned, the default UI + /// vertex shader will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this materials fragment shader. If [`ShaderRef::Default`] is returned, the default + /// UI fragment shader will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + #[inline] + fn specialize(descriptor: &mut RenderPipelineDescriptor, key: UiMaterialKey) {} +} + +pub struct UiMaterialKey { + pub hdr: bool, + pub bind_group_data: M::Data, +} + +impl Eq for UiMaterialKey where M::Data: PartialEq {} + +impl PartialEq for UiMaterialKey +where + M::Data: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.hdr == other.hdr && self.bind_group_data == other.bind_group_data + } +} + +impl Clone for UiMaterialKey +where + M::Data: Clone, +{ + fn clone(&self) -> Self { + Self { + hdr: self.hdr, + bind_group_data: self.bind_group_data, + } + } +} + +impl core::hash::Hash for UiMaterialKey +where + M::Data: core::hash::Hash, +{ + fn hash(&self, state: &mut H) { + self.hdr.hash(state); + self.bind_group_data.hash(state); + } +} + +#[derive( + Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, ExtractComponent, From, +)] +#[reflect(Component, Default)] +#[require(Node)] +pub struct MaterialNode(pub Handle); + +impl Default for MaterialNode { + fn default() -> Self { + Self(Handle::default()) + } +} + +impl From> for AssetId { + fn from(material: MaterialNode) -> Self { + material.id() + } +} + +impl From<&MaterialNode> for AssetId { + fn from(material: &MaterialNode) -> Self { + material.id() + } +} diff --git a/crates/bevy_ui/src/render/ui_material.wgsl b/crates/bevy_ui_render/src/ui_material.wgsl similarity index 100% rename from crates/bevy_ui/src/render/ui_material.wgsl rename to crates/bevy_ui_render/src/ui_material.wgsl diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs similarity index 81% rename from crates/bevy_ui/src/render/ui_material_pipeline.rs rename to crates/bevy_ui_render/src/ui_material_pipeline.rs index 84eb163e4a..eb4f5050cf 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -1,9 +1,8 @@ -use core::{hash::Hash, marker::PhantomData, ops::Range}; - +use crate::ui_material::{MaterialNode, UiMaterial, UiMaterialKey}; use crate::*; use bevy_asset::*; use bevy_ecs::{ - prelude::Component, + prelude::{Component, With}, query::ROQueryItem, system::{ lifetimeless::{Read, SRes}, @@ -11,27 +10,23 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; -use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_render::{ - extract_component::ExtractComponentPlugin, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, TemporaryRenderEntity}, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; +use bevy_render::{RenderApp, RenderStartup}; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; +use bevy_utils::default; use bytemuck::{Pod, Zeroable}; - -pub const UI_MATERIAL_SHADER_HANDLE: Handle = - weak_handle!("b5612b7b-aed5-41b4-a930-1d1588239fcd"); - -const UI_VERTEX_OUTPUT_SHADER_HANDLE: Handle = - weak_handle!("1d97ca3e-eaa8-4bc5-a676-e8e9568c472e"); +use core::{hash::Hash, marker::PhantomData, ops::Range}; /// Adds the necessary ECS resources and render logic to enable rendering entities using the given /// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). @@ -48,22 +43,14 @@ where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - UI_VERTEX_OUTPUT_SHADER_HANDLE, - "ui_vertex_output.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - UI_MATERIAL_SHADER_HANDLE, - "ui_material.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "ui_vertex_output.wgsl"); + + embedded_asset!(app, "ui_material.wgsl"); + app.init_asset::() - .register_type::>() + //.register_type::>() .add_plugins(( - ExtractComponentPlugin::>::extract_visible(), + //ExtractComponentPlugin::>::extract_visible(), RenderAssetPlugin::>::default(), )); @@ -73,6 +60,7 @@ where .init_resource::>() .init_resource::>() .init_resource::>>() + .add_systems(RenderStartup, init_ui_material_pipeline::) .add_systems( ExtractSchedule, extract_ui_material_nodes::.in_set(RenderUiSystems::ExtractBackgrounds), @@ -86,12 +74,6 @@ where ); } } - - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::>(); - } - } } #[derive(Resource)] @@ -135,8 +117,8 @@ pub struct UiMaterialBatch { pub struct UiMaterialPipeline { pub ui_layout: BindGroupLayout, pub view_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, + pub vertex_shader: Handle, + pub fragment_shader: Handle, marker: PhantomData, } @@ -166,15 +148,14 @@ where let mut descriptor = RenderPipelineDescriptor { vertex: VertexState { - shader: UI_MATERIAL_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.vertex_shader.clone(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { - shader: UI_MATERIAL_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -184,34 +165,11 @@ where blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), - layout: vec![], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - }, - depth_stencil: None, - multisample: MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, label: Some("ui_material_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() }; - if let Some(vertex_shader) = &self.vertex_shader { - descriptor.vertex.shader = vertex_shader.clone(); - } - - if let Some(fragment_shader) = &self.fragment_shader { - descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); - } descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()]; @@ -221,39 +179,41 @@ where } } -impl FromWorld for UiMaterialPipeline { - fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - let render_device = world.resource::(); - let ui_layout = M::bind_group_layout(render_device); +pub fn init_ui_material_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let ui_layout = M::bind_group_layout(&render_device); - let view_layout = render_device.create_bind_group_layout( - "ui_view_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::VERTEX_FRAGMENT, - ( - uniform_buffer::(true), - uniform_buffer::(false), - ), + let view_layout = render_device.create_bind_group_layout( + "ui_view_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::VERTEX_FRAGMENT, + ( + uniform_buffer::(true), + uniform_buffer::(false), ), - ); + ), + ); - UiMaterialPipeline { - ui_layout, - view_layout, - vertex_shader: match M::vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - fragment_shader: match M::fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - marker: PhantomData, - } - } + let load_default = || load_embedded_asset!(asset_server.as_ref(), "ui_material.wgsl"); + + commands.insert_resource(UiMaterialPipeline:: { + ui_layout, + view_layout, + vertex_shader: match M::vertex_shader() { + ShaderRef::Default => load_default(), + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), + }, + fragment_shader: match M::fragment_shader() { + ShaderRef::Default => load_default(), + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), + }, + marker: PhantomData, + }); } pub type DrawUiMaterial = ( @@ -296,7 +256,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 { @@ -337,10 +297,10 @@ impl RenderCommand

for DrawUiMaterialNode { pub struct ExtractedUiMaterialNode { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub border: BorderRect, - pub border_radius: ResolvedBorderRadius, + pub border_radius: [f32; 4], pub material: AssetId, pub clip: Option, // Camera to render this UI node to. By the time it is extracted, @@ -348,7 +308,7 @@ pub struct ExtractedUiMaterialNode { // Nodes with ambiguous camera will be ignored. pub extracted_camera_entity: Entity, pub main_entity: MainEntity, - render_entity: Entity, + pub render_entity: Entity, } #[derive(Resource)] @@ -372,7 +332,7 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -403,14 +363,14 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), material: handle.id(), rect: Rect { min: Vec2::ZERO, max: computed_node.size(), }, border: computed_node.border(), - border_radius: computed_node.border_radius(), + border_radius: computed_node.border_radius().into(), clip: clip.map(|clip| clip.clip), extracted_camera_entity, main_entity: entity.into(), @@ -475,10 +435,13 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + extracted_uinode + .transform + .transform_point2(pos * rect_size) + .extend(1.0) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -512,7 +475,7 @@ pub fn prepare_uimaterial_nodes( ]; let transformed_rect_size = - extracted_uinode.transform.transform_vector3(rect_size); + extracted_uinode.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -553,12 +516,7 @@ pub fn prepare_uimaterial_nodes( position: positions_clipped[i].into(), uv: uvs[i].into(), size: extracted_uinode.rect.size().into(), - radius: [ - extracted_uinode.border_radius.top_left, - extracted_uinode.border_radius.top_right, - extracted_uinode.border_radius.bottom_right, - extracted_uinode.border_radius.bottom_left, - ], + radius: extracted_uinode.border_radius, border: [ extracted_uinode.border.left, extracted_uinode.border.top, @@ -598,12 +556,14 @@ impl RenderAsset for PreparedUiMaterial { material: Self::SourceAsset, _: AssetId, (render_device, pipeline, material_param): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { + let bind_group_data = material.bind_group_data(); match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) { Ok(prepared) => Ok(PreparedUiMaterial { bindings: prepared.bindings, bind_group: prepared.bind_group, - key: prepared.data, + key: bind_group_data, }), Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) @@ -653,7 +613,7 @@ pub fn queue_ui_material_nodes( &ui_material_pipeline, UiMaterialKey { hdr: view.hdr, - bind_group_data: material.key.clone(), + bind_group_data: material.key, }, ); if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() { diff --git a/crates/bevy_ui/src/render/ui_texture_slice.wgsl b/crates/bevy_ui_render/src/ui_texture_slice.wgsl similarity index 100% rename from crates/bevy_ui/src/render/ui_texture_slice.wgsl rename to crates/bevy_ui_render/src/ui_texture_slice.wgsl diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs similarity index 90% rename from crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs rename to crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs index 7d0fdb6a42..aa05f106ff 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -2,7 +2,7 @@ use core::{hash::Hash, ops::Range}; use crate::*; use bevy_asset::*; -use bevy_color::{Alpha, ColorToComponents, LinearRgba}; +use bevy_color::{ColorToComponents, LinearRgba}; use bevy_ecs::{ prelude::Component, system::{ @@ -11,38 +11,29 @@ use bevy_ecs::{ }, }; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::HashMap; -use bevy_render::sync_world::MainEntity; use bevy_render::{ render_asset::RenderAssets, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, - sync_world::TemporaryRenderEntity, - texture::{GpuImage, TRANSPARENT_IMAGE_HANDLE}, + texture::GpuImage, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; +use bevy_render::{sync_world::MainEntity, RenderStartup}; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; -use bevy_transform::prelude::GlobalTransform; +use bevy_ui::widget; +use bevy_utils::default; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; -use widget::ImageNode; - -pub const UI_SLICER_SHADER_HANDLE: Handle = - weak_handle!("10cd61e3-bbf7-47fa-91c8-16cbe806378c"); pub struct UiTextureSlicerPlugin; impl Plugin for UiTextureSlicerPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - UI_SLICER_SHADER_HANDLE, - "ui_texture_slice.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "ui_texture_slice.wgsl"); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -51,6 +42,7 @@ impl Plugin for UiTextureSlicerPlugin { .init_resource::() .init_resource::() .init_resource::>() + .add_systems(RenderStartup, init_ui_texture_slice_pipeline) .add_systems( ExtractSchedule, extract_ui_texture_slices.in_set(RenderUiSystems::ExtractTextureSlice), @@ -64,12 +56,6 @@ impl Plugin for UiTextureSlicerPlugin { ); } } - - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::(); - } - } } #[repr(C)] @@ -116,36 +102,38 @@ pub struct UiTextureSliceImageBindGroups { pub struct UiTextureSlicePipeline { pub view_layout: BindGroupLayout, pub image_layout: BindGroupLayout, + pub shader: Handle, } -impl FromWorld for UiTextureSlicePipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); +pub fn init_ui_texture_slice_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let view_layout = render_device.create_bind_group_layout( + "ui_texture_slice_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); - let view_layout = render_device.create_bind_group_layout( - "ui_texture_slice_view_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - uniform_buffer::(true), + let image_layout = render_device.create_bind_group_layout( + "ui_texture_slice_image_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), ), - ); + ), + ); - let image_layout = render_device.create_bind_group_layout( - "ui_texture_slice_image_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: true }), - sampler(SamplerBindingType::Filtering), - ), - ), - ); - - UiTextureSlicePipeline { - view_layout, - image_layout, - } - } + commands.insert_resource(UiTextureSlicePipeline { + view_layout, + image_layout, + shader: load_embedded_asset!(asset_server.as_ref(), "ui_texture_slice.wgsl"), + }); } #[derive(Clone, Copy, Hash, PartialEq, Eq)] @@ -180,15 +168,14 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: UI_SLICER_SHADER_HANDLE, - entry_point: "vertex".into(), + shader: self.shader.clone(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { - shader: UI_SLICER_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -198,33 +185,18 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.image_layout.clone()], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - }, - depth_stencil: None, - multisample: MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, label: Some("ui_texture_slice_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } pub struct ExtractedUiTextureSlice { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, @@ -252,7 +224,7 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -312,7 +284,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), color: image.color.into(), rect: Rect { min: Vec2::ZERO, @@ -372,9 +344,7 @@ pub fn queue_ui_slices( draw_function, pipeline, entity: (extracted_slicer.render_entity, extracted_slicer.main_entity), - sort_key: FloatOrd( - extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE, - ), + sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, @@ -503,11 +473,12 @@ pub fn prepare_ui_slices( let uinode_rect = texture_slices.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -542,7 +513,7 @@ pub fn prepare_ui_slices( ]; let transformed_rect_size = - texture_slices.transform.transform_vector3(rect_size); + texture_slices.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/ui_vertex_output.wgsl b/crates/bevy_ui_render/src/ui_vertex_output.wgsl similarity index 100% rename from crates/bevy_ui/src/render/ui_vertex_output.wgsl rename to crates/bevy_ui_render/src/ui_vertex_output.wgsl diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 39ba4629a5..447c9966f4 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -1,40 +1,34 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["std", "serde"] +default = ["parallel"] -# Functionality +wgpu_wrapper = ["dep:send_wrapper"] -## Adds serialization support through `serde`. -serde = ["bevy_platform/serialize"] +# Provides access to the `Parallel` type. +parallel = ["bevy_platform/std", "dep:thread_local"] -# Platform Compatibility +std = ["disqualified/alloc"] -## Allows access to the `std` crate. Enabling this feature will prevent compilation -## on `no_std` targets, but provides access to certain additional features on -## supported platforms. -std = ["alloc", "bevy_platform/std", "dep:thread_local"] - -## Allows access to the `alloc` crate. -alloc = ["bevy_platform/alloc"] - -## `critical-section` provides the building blocks for synchronization primitives -## on all platforms, including `no_std`. -critical-section = ["bevy_platform/critical-section"] +debug = ["bevy_platform/alloc"] [dependencies] -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 } +disqualified = { version = "1.0", default-features = false } thread_local = { version = "1.0", optional = true } +[target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] +send_wrapper = { version = "0.6.0", optional = true } + [dev-dependencies] static_assertions = "1.1.0" diff --git a/crates/bevy_utils/README.md b/crates/bevy_utils/README.md index 341755babc..4515209dbe 100644 --- a/crates/bevy_utils/README.md +++ b/crates/bevy_utils/README.md @@ -6,4 +6,4 @@ [![Docs](https://docs.rs/bevy_utils/badge.svg)](https://docs.rs/bevy_utils/latest/bevy_utils/) [![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) -A Collection of Utilities for the [Bevy Engine](https://bevyengine.org/). +A Collection of Utilities for the [Bevy Engine](https://bevy.org/). diff --git a/crates/bevy_utils/src/debug_info.rs b/crates/bevy_utils/src/debug_info.rs new file mode 100644 index 0000000000..71ce96ea00 --- /dev/null +++ b/crates/bevy_utils/src/debug_info.rs @@ -0,0 +1,129 @@ +use crate::cfg; +cfg::alloc! { + 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: &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>, +} + +cfg::alloc! { + 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, + reason = "The value will be ignored if the `debug` feature is not enabled" + ) + )] + pub const fn borrowed(value: &'static str) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Borrowed(value), + } + } + + cfg::alloc! { + /// 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, + reason = "The value will be ignored if the `debug` feature is not enabled" + ) + )] + 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`] corresponding 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() + } +} + +cfg::alloc! { + impl From> for DebugName { + #[cfg_attr( + not(feature = "debug"), + expect( + unused_variables, + reason = "The value will be ignored if the `debug` feature is not enabled" + ) + )] + 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/default.rs b/crates/bevy_utils/src/default.rs index 5b4b9fbdf9..0ca45d544d 100644 --- a/crates/bevy_utils/src/default.rs +++ b/crates/bevy_utils/src/default.rs @@ -12,7 +12,7 @@ /// } /// /// // Normally you would initialize a struct with defaults using "struct update syntax" -/// // combined with `Default::default()`. This example sets `Foo::bar` to 10 and the remaining +/// // combined with `Default::default()`. This example sets `Foo::a` to 10 and the remaining /// // values to their defaults. /// let foo = Foo { /// a: 10, diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 9f564f14c2..58979139bb 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -1,91 +1,69 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] //! General utilities for first-party [Bevy] engine crates. //! -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ -#[cfg(feature = "std")] -extern crate std; +/// Configuration information for this crate. +pub mod cfg { + pub(crate) use bevy_platform::cfg::*; -#[cfg(feature = "alloc")] -extern crate alloc; + pub use bevy_platform::cfg::{alloc, std}; + + define_alias! { + #[cfg(feature = "parallel")] => { + /// Indicates the `Parallel` type is available. + parallel + } + } +} + +cfg::std! { + extern crate std; +} + +cfg::alloc! { + extern crate alloc; + + mod map; + pub use map::*; +} + +cfg::parallel! { + mod parallel_queue; + pub use parallel_queue::*; +} /// The utilities prelude. /// /// 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; } -pub mod synccell; -pub mod syncunsafecell; +#[cfg(feature = "wgpu_wrapper")] +mod wgpu_wrapper; +mod debug_info; mod default; mod once; -#[cfg(feature = "std")] -mod parallel_queue; #[doc(hidden)] pub use once::OnceFlag; pub use default::default; -#[cfg(feature = "std")] -pub use parallel_queue::*; +#[cfg(feature = "wgpu_wrapper")] +pub use wgpu_wrapper::WgpuWrapper; use core::mem::ManuallyDrop; -#[cfg(feature = "alloc")] -use { - bevy_platform::{ - collections::HashMap, - hash::{Hashed, NoOpHash, PassHash}, - }, - core::{any::TypeId, hash::Hash}, -}; - -/// A [`HashMap`] pre-configured to use [`Hashed`] keys and [`PassHash`] passthrough hashing. -/// Iteration order only depends on the order of insertions and deletions. -#[cfg(feature = "alloc")] -pub type PreHashMap = HashMap, V, PassHash>; - -/// Extension methods intended to add functionality to [`PreHashMap`]. -#[cfg(feature = "alloc")] -pub trait PreHashMapExt { - /// Tries to get or insert the value for the given `key` using the pre-computed hash first. - /// If the [`PreHashMap`] does not already contain the `key`, it will clone it and insert - /// the value returned by `func`. - fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V; -} - -#[cfg(feature = "alloc")] -impl PreHashMapExt for PreHashMap { - #[inline] - fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V { - use bevy_platform::collections::hash_map::RawEntryMut; - let entry = self - .raw_entry_mut() - .from_key_hashed_nocheck(key.hash(), key); - match entry { - RawEntryMut::Occupied(entry) => entry.into_mut(), - RawEntryMut::Vacant(entry) => { - let (_, value) = entry.insert_hashed_nocheck(key.hash(), key.clone(), func()); - value - } - } - } -} - -/// A specialized hashmap type with Key of [`TypeId`] -/// Iteration order only depends on the order of insertions and deletions. -#[cfg(feature = "alloc")] -pub type TypeIdMap = HashMap; - /// A type which calls a function when dropped. /// This can be used to ensure that cleanup code is run even in case of a panic. /// @@ -144,46 +122,3 @@ impl Drop for OnDrop { callback(); } } - -#[cfg(test)] -mod tests { - use super::*; - use static_assertions::assert_impl_all; - - // Check that the HashMaps are Clone if the key/values are Clone - assert_impl_all!(PreHashMap::: Clone); - - #[test] - fn fast_typeid_hash() { - struct Hasher; - - impl core::hash::Hasher for Hasher { - fn finish(&self) -> u64 { - 0 - } - fn write(&mut self, _: &[u8]) { - panic!("Hashing of core::any::TypeId changed"); - } - fn write_u64(&mut self, _: u64) {} - } - - Hash::hash(&TypeId::of::<()>(), &mut Hasher); - } - - #[cfg(feature = "alloc")] - #[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 { - map_1.insert(i, i); - map_2.insert(i, i); - } - assert_eq!( - map_1.iter().collect::>(), - map_2.iter().collect::>() - ); - } -} diff --git a/crates/bevy_utils/src/map.rs b/crates/bevy_utils/src/map.rs new file mode 100644 index 0000000000..3b54a357aa --- /dev/null +++ b/crates/bevy_utils/src/map.rs @@ -0,0 +1,155 @@ +use core::{any::TypeId, hash::Hash}; + +use bevy_platform::{ + collections::{hash_map::Entry, HashMap}, + hash::{Hashed, NoOpHash, PassHash}, +}; + +/// A [`HashMap`] pre-configured to use [`Hashed`] keys and [`PassHash`] passthrough hashing. +/// Iteration order only depends on the order of insertions and deletions. +pub type PreHashMap = HashMap, V, PassHash>; + +/// Extension methods intended to add functionality to [`PreHashMap`]. +pub trait PreHashMapExt { + /// Tries to get or insert the value for the given `key` using the pre-computed hash first. + /// If the [`PreHashMap`] does not already contain the `key`, it will clone it and insert + /// the value returned by `func`. + fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V; +} + +impl PreHashMapExt for PreHashMap { + #[inline] + fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V { + use bevy_platform::collections::hash_map::RawEntryMut; + let entry = self + .raw_entry_mut() + .from_key_hashed_nocheck(key.hash(), key); + match entry { + RawEntryMut::Occupied(entry) => entry.into_mut(), + RawEntryMut::Vacant(entry) => { + let (_, value) = entry.insert_hashed_nocheck(key.hash(), key.clone(), func()); + value + } + } + } +} + +/// A specialized hashmap type with Key of [`TypeId`] +/// Iteration order only depends on the order of insertions and deletions. +pub type TypeIdMap = 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::*; + use static_assertions::assert_impl_all; + + // Check that the HashMaps are Clone if the key/values are Clone + assert_impl_all!(PreHashMap::: Clone); + + #[test] + fn fast_typeid_hash() { + struct Hasher; + + impl core::hash::Hasher for Hasher { + fn finish(&self) -> u64 { + 0 + } + fn write(&mut self, _: &[u8]) { + panic!("Hashing of core::any::TypeId changed"); + } + fn write_u64(&mut self, _: u64) {} + } + + Hash::hash(&TypeId::of::<()>(), &mut Hasher); + } + + crate::cfg::alloc! { + #[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 { + map_1.insert(i, i); + map_2.insert(i, i); + } + assert_eq!( + map_1.iter().collect::>(), + map_2.iter().collect::>() + ); + } + } +} \ No newline at end of file diff --git a/crates/bevy_utils/src/parallel_queue.rs b/crates/bevy_utils/src/parallel_queue.rs index 861d17bcf2..8f35b4554d 100644 --- a/crates/bevy_utils/src/parallel_queue.rs +++ b/crates/bevy_utils/src/parallel_queue.rs @@ -5,12 +5,10 @@ use thread_local::ThreadLocal; /// A cohesive set of thread-local values of a given type. /// /// Mutable references can be fetched if `T: Default` via [`Parallel::scope`]. -#[derive(Default)] pub struct Parallel { locals: ThreadLocal>, } -/// A scope guard of a `Parallel`, when this struct is dropped ,the value will writeback to its `Parallel` impl Parallel { /// Gets a mutable iterator over all of the per-thread queues. pub fn iter_mut(&mut self) -> impl Iterator { @@ -21,6 +19,25 @@ impl Parallel { pub fn clear(&mut self) { self.locals.clear(); } + + /// Retrieves the thread-local value for the current thread and runs `f` on it. + /// + /// If there is no thread-local value, it will be initialized to the result + /// of `create`. + pub fn scope_or(&self, create: impl FnOnce() -> T, f: impl FnOnce(&mut T) -> R) -> R { + f(&mut self.borrow_local_mut_or(create)) + } + + /// Mutably borrows the thread-local value. + /// + /// If there is no thread-local value, it will be initialized to the result + /// of `create`. + pub fn borrow_local_mut_or( + &self, + create: impl FnOnce() -> T, + ) -> impl DerefMut + '_ { + self.locals.get_or(|| RefCell::new(create())).borrow_mut() + } } impl Parallel { @@ -28,15 +45,14 @@ impl Parallel { /// /// If there is no thread-local value, it will be initialized to its default. pub fn scope(&self, f: impl FnOnce(&mut T) -> R) -> R { - let mut cell = self.locals.get_or_default().borrow_mut(); - f(cell.deref_mut()) + self.scope_or(Default::default, f) } /// Mutably borrows the thread-local value. /// - /// If there is no thread-local value, it will be initialized to it's default. + /// If there is no thread-local value, it will be initialized to its default. pub fn borrow_local_mut(&self) -> impl DerefMut + '_ { - self.locals.get_or_default().borrow_mut() + self.borrow_local_mut_or(Default::default) } } @@ -56,7 +72,6 @@ where } } -#[cfg(feature = "alloc")] impl Parallel> { /// Collect all enqueued items from all threads and appends them to the end of a /// single Vec. @@ -74,3 +89,12 @@ impl Parallel> { } } } + +// `Default` is manually implemented to avoid the `T: Default` bound. +impl Default for Parallel { + fn default() -> Self { + Self { + locals: ThreadLocal::default(), + } + } +} diff --git a/crates/bevy_utils/src/wgpu_wrapper.rs b/crates/bevy_utils/src/wgpu_wrapper.rs new file mode 100644 index 0000000000..272d0dd4c0 --- /dev/null +++ b/crates/bevy_utils/src/wgpu_wrapper.rs @@ -0,0 +1,50 @@ +/// A wrapper to safely make `wgpu` types Send / Sync on web with atomics enabled. +/// +/// On web with `atomics` enabled the inner value can only be accessed +/// or dropped on the `wgpu` thread or else a panic will occur. +/// On other platforms the wrapper simply contains the wrapped value. +#[derive(Debug, Clone)] +pub struct WgpuWrapper( + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] T, + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] send_wrapper::SendWrapper, +); + +// SAFETY: SendWrapper is always Send + Sync. +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[expect(unsafe_code, reason = "Blanket-impl Send requires unsafe.")] +unsafe impl Send for WgpuWrapper {} +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[expect(unsafe_code, reason = "Blanket-impl Sync requires unsafe.")] +unsafe impl Sync for WgpuWrapper {} + +impl WgpuWrapper { + /// Constructs a new instance of `WgpuWrapper` which will wrap the specified value. + pub fn new(t: T) -> Self { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return Self(t); + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return Self(send_wrapper::SendWrapper::new(t)); + } + + /// Unwraps the value. + pub fn into_inner(self) -> T { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return self.0; + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return self.0.take(); + } +} + +impl core::ops::Deref for WgpuWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::DerefMut for WgpuWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index b2b6d730fe..2cd96053b4 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -22,12 +22,7 @@ bevy_reflect = [ ] ## Adds serialization support through `serde`. -serialize = [ - "serde", - "smol_str/serde", - "bevy_ecs/serialize", - "bevy_input/serialize", -] +serialize = ["serde", "bevy_ecs/serialize", "bevy_input/serialize"] # Platform Compatibility @@ -50,16 +45,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_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_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1.0", features = [ @@ -69,7 +62,6 @@ serde = { version = "1.0", features = [ raw-window-handle = { version = "0.6", features = [ "alloc", ], default-features = false } -smol_str = { version = "0.2", default-features = false } log = { version = "0.4", default-features = false } [target.'cfg(target_os = "android")'.dependencies] 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 21ee8d64c9..22e657cf03 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -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 31ff212ebe..4fc039d7c7 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,4 +1,6 @@ -use alloc::{borrow::ToOwned, format, string::String}; +#[cfg(feature = "std")] +use alloc::format; +use alloc::{borrow::ToOwned, string::String}; use core::num::NonZero; use bevy_ecs::{ @@ -158,10 +160,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. @@ -225,6 +225,15 @@ pub struct Window { /// You should also set the window `composite_alpha_mode` to `CompositeAlphaMode::PostMultiplied`. pub transparent: bool, /// Get/set whether the window is focused. + /// + /// It cannot be set unfocused after creation. + /// + /// ## Platform-specific + /// + /// - iOS / Android / X11 / Wayland: Spawning unfocused is + /// [not supported](https://docs.rs/winit/latest/winit/window/struct.WindowAttributes.html#method.with_active). + /// - iOS / Android / Web / Wayland: Setting focused after creation is + /// [not supported](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.focus_window). pub focused: bool, /// Where should the window appear relative to other overlapping window. /// @@ -461,7 +470,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(), @@ -697,15 +705,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; } @@ -719,11 +725,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( @@ -745,11 +751,11 @@ pub struct CursorOptions { /// /// ## Platform-specific /// - /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] + /// - **`X11`** doesn't support [`CursorGrabMode::Locked`] /// - **`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` and `X11` don'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. @@ -1058,11 +1064,11 @@ impl From for WindowResolution { /// /// ## Platform-specific /// -/// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] +/// - **`X11`** doesn't support [`CursorGrabMode::Locked`] /// - **`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` and `X11` don'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", @@ -1162,13 +1168,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, @@ -1463,7 +1473,8 @@ pub struct ClosingWindow; /// - Only used on iOS. /// /// [`winit::platform::ios::ScreenEdge`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/struct.ScreenEdge.html -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum ScreenEdge { #[default] diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 5665a96029..3ad2e4379e 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -16,7 +16,6 @@ x11 = ["winit/x11"] accesskit_unix = ["accesskit_winit/accesskit_unix", "accesskit_winit/async-io"] serialize = [ - "serde", "bevy_input/serialize", "bevy_window/serialize", "bevy_platform/serialize", @@ -28,39 +27,37 @@ 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_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 winit = { version = "0.30", default-features = false, features = ["rwh_06"] } -accesskit_winit = { version = "0.25", default-features = false, features = [ +accesskit_winit = { version = "0.27", default-features = false, features = [ "rwh_06", ] } approx = { version = "0.5", default-features = false } 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 } -accesskit = "0.18" +wgpu-types = { version = "25", optional = true } +accesskit = "0.19" tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -68,16 +65,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 bdca3f8585..c5c5e489a6 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -22,11 +22,12 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - observer::Trigger, + lifecycle::Remove, + observer::On, query::With, reflect::ReflectComponent, system::{Commands, Local, Query}, - world::{OnRemove, Ref}, + world::Ref, }; #[cfg(feature = "custom_cursor")] use bevy_image::{Image, TextureAtlasLayout}; @@ -191,7 +192,7 @@ 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()) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index d7c880a9b9..8926095dc0 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! `bevy_winit` provides utilities to handle window creation and the eventloop through [`winit`] @@ -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 1855539cc5..97c26db7d8 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,10 +106,11 @@ struct WinitAppRunnerState { )>, } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn new(mut app: App) -> Self { + app.add_event::(); #[cfg(feature = "custom_cursor")] - app.add_event::().init_resource::(); + app.init_resource::(); let event_writer_system_state: SystemState<( EventWriter, @@ -197,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; @@ -239,7 +240,7 @@ impl ApplicationHandler for WinitAppRunnerState { fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: T) { self.user_event_received = true; - self.world_mut().send_event(event); + self.world_mut().write_event(event); self.redraw_requested = true; } @@ -473,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(); } } }); @@ -548,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(); @@ -604,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| { @@ -621,6 +624,7 @@ impl WinitAppRunnerState { event_loop, entity, &window, + &cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -786,91 +790,91 @@ impl WinitAppRunnerState { if !raw_winit_events.is_empty() { world .resource_mut::>() - .send_batch(raw_winit_events); + .write_batch(raw_winit_events); } for winit_event in buffered_events.iter() { match winit_event.clone() { BevyWindowEvent::AppLifecycle(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::CursorEntered(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::CursorLeft(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::CursorMoved(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::FileDragAndDrop(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::Ime(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::RequestRedraw(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowBackendScaleFactorChanged(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowCloseRequested(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowCreated(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowDestroyed(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowFocused(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowMoved(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowOccluded(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowResized(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowScaleFactorChanged(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::WindowThemeChanged(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::MouseButtonInput(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::MouseMotion(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::MouseWheel(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::PinchGesture(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::RotationGesture(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::DoubleTapGesture(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::PanGesture(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::TouchInput(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::KeyboardInput(e) => { - world.send_event(e); + world.write_event(e); } BevyWindowEvent::KeyboardFocusLost(e) => { - world.send_event(e); + world.write_event(e); } } } @@ -878,7 +882,7 @@ impl WinitAppRunnerState { if !buffered_events.is_empty() { world .resource_mut::>() - .send_batch(buffered_events); + .write_batch(buffered_events); } } @@ -933,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 97483c7358..6d3a76c9b3 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,18 +1,20 @@ use std::collections::HashMap; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ + change_detection::DetectChangesMut, entity::Entity, event::EventWriter, + lifecycle::RemovedComponents, prelude::{Changed, Component}, query::QueryFilter, - removal_detection::RemovedComponents, system::{Local, NonSendMarker, Query, SystemParamItem}, }; 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 d666491311..8cc5dcfac7 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, @@ -129,7 +130,8 @@ impl WinitWindows { .with_resizable(window.resizable) .with_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)) .with_decorations(window.decorations) - .with_transparent(window.transparent); + .with_transparent(window.transparent) + .with_active(window.focused); #[cfg(target_os = "windows")] { @@ -188,11 +190,16 @@ impl WinitWindows { bevy_log::debug!("{display_info}"); #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", + all( + any(feature = "wayland", feature = "x11"), + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ), target_os = "windows" ))] if let Some(name) = &window.name { @@ -283,7 +290,7 @@ impl WinitWindows { let canvas = canvas.dyn_into::().ok(); winit_window_attributes = winit_window_attributes.with_canvas(canvas); } else { - panic!("Cannot find element: {}.", selector); + panic!("Cannot find element: {selector}."); } } @@ -309,16 +316,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 @@ -527,7 +534,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-template/EXAMPLE_README.md.tpl b/docs-template/EXAMPLE_README.md.tpl index a5438464b3..dfd516303b 100644 --- a/docs-template/EXAMPLE_README.md.tpl +++ b/docs-template/EXAMPLE_README.md.tpl @@ -269,6 +269,7 @@ Bevy has a helper to build its examples: - Build for WebGL2: `cargo run -p build-wasm-example -- --api webgl2 load_gltf` - Build for WebGPU: `cargo run -p build-wasm-example -- --api webgpu load_gltf` +- Debug: `cargo run -p build-wasm-example -- --debug --api webgl2 load_gltf` This helper will log the command used to build the examples. @@ -282,7 +283,7 @@ In browsers, audio is not authorized to start without being triggered by an user On the web, it's useful to reduce the size of the files that are distributed. With rust, there are many ways to improve your executable sizes, starting with -the steps described in [the quick-start guide](https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). +the steps described in [the quick-start guide](https://bevy.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). Now, when building the executable, use `--profile wasm-release` instead of `--release`: diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f15fa1c4c6..120c461efe 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| @@ -37,9 +38,11 @@ The default feature set enables most of the expected features of a game engine, |bevy_text|Provides text functionality| |bevy_ui|A custom ECS-driven UI framework| |bevy_ui_picking_backend|Provides an implementation for picking UI| +|bevy_ui_render|Provides rendering functionality for bevy_ui| |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| @@ -52,7 +55,7 @@ The default feature set enables most of the expected features of a game engine, |vorbis|OGG/VORBIS audio format support| |webgl2|Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU.| |x11|X11 display server support| -|zstd|For KTX2 supercompression| +|zstd_rust|For KTX2 Zstandard decompression using pure rust [ruzstd](https://crates.io/crates/ruzstd). This is the safe default. For maximum performance, use "zstd_c".| ### Optional Features @@ -68,9 +71,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| -|configurable_error_handler|Use the configurable global error handler as the default error handler.| +|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| @@ -78,6 +82,7 @@ The default feature set enables most of the expected features of a game engine, |detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in| |dynamic_linking|Force dynamic linking, which improves iterative compile times| |embedded_watcher|Enables watching in memory asset providers for Bevy Asset hot-reloading| +|experimental_bevy_feathers|Feathers widget collection.| |experimental_pbr_pcss|Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs| |exr|EXR image format support| |ff|Farbfeld image format support| @@ -86,6 +91,8 @@ The default feature set enables most of the expected features of a game engine, |ghost_nodes|Experimental support for nodes that are ignored for UI layouting| |gif|GIF image format support| |glam_assert|Enable assertions to check the validity of parameters passed to glam| +|gltf_convert_coordinates_default|Enable converting glTF coordinates to Bevy's coordinate system by default. This will be Bevy's default behavior starting in 0.18.| +|hotpatching|Enable hotpatching of Bevy systems| |ico|ICO image format support| |jpeg|JPEG image format support| |libm|Uses the `libm` maths library instead of the one provided in `std` and `core`.| @@ -94,6 +101,8 @@ The default feature set enables most of the expected features of a game engine, |minimp3|MP3 audio format support (through minimp3)| |mp3|MP3 audio format support| |pbr_anisotropy_texture|Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| +|pbr_clustered_decals|Enable support for Clustered Decals| +|pbr_light_textures|Enable support for Light Textures| |pbr_multi_layer_material_textures|Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_specular_textures|Enable support for specular textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| @@ -126,3 +135,4 @@ The default feature set enables most of the expected features of a game engine, |webgpu|Enable support for WebGPU in Wasm. When enabled, this feature will override the `webgl2` feature and you won't be able to run Wasm builds with WebGL2, only with WebGPU.| |webp|WebP image format support| |zlib|For KTX2 supercompression| +|zstd_c|For KTX2 Zstandard decompression using [zstd](https://crates.io/crates/zstd). This is a faster backend, but uses unsafe C bindings. For the safe option, stick to the default backend with "zstd_rust".| diff --git a/docs/linux_dependencies.md b/docs/linux_dependencies.md index 53ad5257e8..a5a43c3d5f 100644 --- a/docs/linux_dependencies.md +++ b/docs/linux_dependencies.md @@ -91,10 +91,10 @@ Set the `PKG_CONFIG_PATH` env var to `/usr/lib//pkgconfig/`. For example export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig/" ``` -## Arch / Manjaro +## [Arch](https://archlinux.org/) / [Manjaro](https://manjaro.org/) ```bash -sudo pacman -S libx11 pkgconf alsa-lib +sudo pacman -S libx11 pkgconf alsa-lib libxcursor libxrandr libxi ``` Install `pipewire-alsa` or `pulseaudio-alsa` depending on the sound server you are using. @@ -102,7 +102,7 @@ Install `pipewire-alsa` or `pulseaudio-alsa` depending on the sound server you a Depending on your graphics card, you may have to install one of the following: `vulkan-radeon`, `vulkan-intel`, or `mesa-vulkan-drivers` -## Void +## [Void](https://voidlinux.org/) ```bash sudo xbps-install -S pkgconf alsa-lib-devel libX11-devel eudev-libudev-devel @@ -110,6 +110,80 @@ sudo xbps-install -S pkgconf alsa-lib-devel libX11-devel eudev-libudev-devel ## [Nix](https://nixos.org) +### flake.nix + +Add a `flake.nix` file to the root of your GitHub repository containing: + +```nix +{ + description = "bevy flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + nixpkgs, + rust-overlay, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + in + { + devShells.default = + with pkgs; + mkShell { + buildInputs = + [ + # Rust dependencies + (rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; }) + pkg-config + ] + ++ lib.optionals (lib.strings.hasInfix "linux" system) [ + # for Linux + # Audio (Linux only) + alsa-lib + # Cross Platform 3D Graphics API + vulkan-loader + # For debugging around vulkan + vulkan-tools + # Other dependencies + libudev-zero + xorg.libX11 + xorg.libXcursor + xorg.libXi + xorg.libXrandr + libxkbcommon + ]; + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + LD_LIBRARY_PATH = lib.makeLibraryPath [ + vulkan-loader + xorg.libX11 + xorg.libXi + xorg.libXcursor + libxkbcommon + ]; + }; + } + ); +} +``` + +> [!TIP] +> We have confirmed that this flake.nix can be used successfully on NixOS and MacOS with Rust's edition set to 2021. + +### shell.nix + Add a `shell.nix` file to the root of the project containing: ```nix @@ -138,8 +212,8 @@ If running nix on a non NixOS system (such as ubuntu, arch etc.), [NixGL](https: to link graphics drivers into the context of software installed by nix: 1. Install a system specific nixGL wrapper ([docs](https://github.com/nix-community/nixGL)). - * If you're running a nvidia GPU choose `nixVulkanNvidia`. - * Otherwise, choose another wrapper appropriate for your system. + - If you're running a nvidia GPU choose `nixVulkanNvidia`. + - Otherwise, choose another wrapper appropriate for your system. 2. Run `nixVulkanNvidia-xxx.xxx.xx cargo run` to compile a bevy program, where `xxx-xxx-xx` denotes the graphics driver version `nixVulkanNvidia` was compiled with. This is also possible with [Nix flakes](https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html). @@ -152,7 +226,7 @@ for more information about `devShells`. Note that this template does not add Rust to the environment because there are many ways to do it. For example, to use stable Rust from nixpkgs, you can add `cargo` and `rustc` to `nativeBuildInputs`. -[Here]([https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/jumpy/package.nix](https://github.com/NixOS/nixpkgs/blob/0da3c44a9460a26d2025ec3ed2ec60a895eb1114/pkgs/games/jumpy/default.nix)) +[Here](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/jumpy/package.nix) is an example of packaging a Bevy program in nix. ## [OpenSUSE](https://www.opensuse.org/) @@ -161,7 +235,7 @@ is an example of packaging a Bevy program in nix. sudo zypper install libudev-devel gcc-c++ alsa-lib-devel ``` -## Gentoo +## [Gentoo](https://www.gentoo.org/) ```bash sudo emerge --ask libX11 pkgconf alsa-lib diff --git a/docs/profiling.md b/docs/profiling.md index ef6a46a608..fe55b048af 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -10,6 +10,7 @@ - [Perf flame graph](#perf-flame-graph) - [GPU runtime](#gpu-runtime) - [Vendor tools](#vendor-tools) + - [Xcode's Metal debugger](#xcodes-metal-debugger) - [Tracy RenderQueue](#tracy-renderqueue) - [Compile time](#compile-time) @@ -147,6 +148,33 @@ For profiling GPU work, you should use the tool corresponding to your GPU's vend Note that while RenderDoc is a great debugging tool, it is _not_ a profiler, and should not be used for this purpose. +#### Xcode's Metal debugger + +Follow the steps below to start GPU debugging on macOS. There is no need to create an Xcode project. + +1. In the menu bar click on Debug > Debug Executable… + + ![Xcode's menu bar open to Debug > Debug Executable...](https://github.com/user-attachments/assets/efdc5037-0957-4227-b29d-9a789ba17a0a) + +2. Select your executable from your project’s target folder. +3. The Scheme Editor will open. If your assets are not located next to your executable, you can go to the Arguments tab and set `BEVY_ASSET_ROOT` to the absolute path for your project (the parent of your assets folder). The rest of the defaults should be fine. + + ![Xcode's Schema Editor opened to an environment variable configuration](https://github.com/user-attachments/assets/29cafb05-0c49-4777-8d41-8643812e8f6a) + +4. Click the play button in the top left and this should start your bevy app. + + ![A cursor hovering over the play button in XCode](https://github.com/user-attachments/assets/859580e2-779b-4db8-8ea6-73cf4ef696c9) + +5. Go back to Xcode and click on the Metal icon in the bottom drawer and then Capture in the following the popup menu. + + ![A cursor hovering over the Capture button in the Metal debugging popup menu](https://github.com/user-attachments/assets/c0ce1591-0a53-499b-bd1b-4d89538ea248) + +6. Start debugging and profiling! + +![Xcode open to the Performance tab in the Debug Navigator.](https://github.com/user-attachments/assets/52732391-9306-44a9-ae01-dcf4573f77ab) + +These instructions were created for Xcode 16.4. + ### Tracy RenderQueue While it doesn't provide as much detail as vendor-specific tooling, Tracy can also be used to coarsely measure GPU performance. diff --git a/errors/README.md b/errors/README.md index be2adb2b00..fe25e78400 100644 --- a/errors/README.md +++ b/errors/README.md @@ -3,6 +3,6 @@ This crate lists and tests explanations and examples of Bevy's error codes. For the latest Bevy release, you can find a rendered version of the error code descriptions at -[bevyengine.org/learn/errors]. +[bevy.org/learn/errors]. -[bevyengine.org/learn/errors]: https://bevyengine.org/learn/errors +[bevy.org/learn/errors]: https://bevy.org/learn/errors diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index dbdd846eba..f69e138364 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -1,4 +1,14 @@ -//! Shows how to render simple primitive shapes with a single color. +//! Here we use shape primitives to build meshes in a 2D rendering context, making each mesh a certain color by giving that mesh's entity a material based off a [`Color`]. +//! +//! Meshes are better known for their use in 3D rendering, but we can use them in a 2D context too. Without a third dimension, the meshes we're building are flat – like paper on a table. These are still very useful for "vector-style" graphics, picking behavior, or as a foundation to build off of for where to apply a shader. +//! +//! A "shape definition" is not a mesh on its own. A circle can be defined with a radius, i.e. [`Circle::new(50.0)`][Circle::new], but rendering tends to happen with meshes built out of triangles. So we need to turn shape descriptions into meshes. +//! +//! Thankfully, we can add shape primitives directly to [`Assets`] because [`Mesh`] implements [`From`] for shape primitives and [`Assets::add`] can be given any value that can be "turned into" `T`! +//! +//! We apply a material to the shape by first making a [`Color`] then calling [`Assets::add`] with that color as its argument, which will create a material from that color through the same process [`Assets::add`] can take a shape primitive. +//! +//! Both the mesh and material need to be wrapped in their own "newtypes". The mesh and material are currently [`Handle`] and [`Handle`] at the moment, which are not components. Handles are put behind "newtypes" to prevent ambiguity, as some entities might want to have handles to meshes (or images, or materials etc.) for different purposes! All we need to do to make them rendering-relevant components is wrap the mesh handle and the material handle in [`Mesh2d`] and [`MeshMaterial2d`] respectively. //! //! You can toggle wireframes with the space bar except on wasm. Wasm does not support //! `POLYGON_MODE_LINE` on the gpu. diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index fd90210d6f..9fb8791ed5 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -25,7 +25,6 @@ fn setup( commands.spawn(( Camera2d, Camera { - hdr: true, // 1. HDR is required for bloom clear_color: ClearColorConfig::Custom(Color::BLACK), ..default() }, @@ -191,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/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 6354aa468c..58ec9b5ceb 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -6,7 +6,6 @@ //! [`Material2d`]: bevy::sprite::Material2d use bevy::{ - asset::weak_handle, color::palettes::basic::YELLOW, core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, math::{ops, FloatOrd}, @@ -20,10 +19,10 @@ use bevy::{ }, render_resource::{ BlendState, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, - DepthStencilState, Face, FragmentState, FrontFace, MultisampleState, PipelineCache, - PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineDescriptor, - SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState, - TextureFormat, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, + DepthStencilState, Face, FragmentState, MultisampleState, PipelineCache, + PrimitiveState, PrimitiveTopology, RenderPipelineDescriptor, SpecializedRenderPipeline, + SpecializedRenderPipelines, StencilFaceState, StencilState, TextureFormat, + VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, }, sync_component::SyncComponentPlugin, sync_world::{MainEntityHashMap, RenderEntity}, @@ -129,12 +128,16 @@ pub struct ColoredMesh2d; pub struct ColoredMesh2dPipeline { /// This pipeline wraps the standard [`Mesh2dPipeline`] mesh2d_pipeline: Mesh2dPipeline, + /// The shader asset handle. + shader: Handle, } impl FromWorld for ColoredMesh2dPipeline { fn from_world(world: &mut World) -> Self { Self { mesh2d_pipeline: Mesh2dPipeline::from_world(world), + // Get the shader from the shader resource we inserted in the plugin. + shader: world.resource::().0.clone(), } } } @@ -164,22 +167,20 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { RenderPipelineDescriptor { vertex: VertexState { // Use our custom shader - shader: COLORED_MESH2D_SHADER_HANDLE, - entry_point: "vertex".into(), - shader_defs: vec![], + shader: self.shader.clone(), // Use our custom vertex buffer buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { // Use our custom shader - shader: COLORED_MESH2D_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "fragment".into(), + shader: self.shader.clone(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), // Use the two standard uniforms for 2d meshes layout: vec![ @@ -188,15 +189,10 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { // Bind group 1 is the mesh uniform self.mesh2d_pipeline.mesh_layout.clone(), ], - push_constant_ranges: vec![], primitive: PrimitiveState { - front_face: FrontFace::Ccw, cull_mode: Some(Face::Back), - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, topology: key.primitive_topology(), - strip_index_format: None, + ..default() }, depth_stencil: Some(DepthStencilState { format: CORE_2D_DEPTH_FORMAT, @@ -220,7 +216,7 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { alpha_to_coverage_enabled: false, }, label: Some("colored_mesh2d_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -285,9 +281,10 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { /// Plugin that renders [`ColoredMesh2d`]s pub struct ColoredMesh2dPlugin; -/// Handle to the custom shader with a unique random ID -pub const COLORED_MESH2D_SHADER_HANDLE: Handle = - weak_handle!("f48b148f-7373-4638-9900-392b3b3ccc66"); +/// A resource holding the shader asset handle for the pipeline to take. There are many ways to get +/// the shader into the pipeline - this is just one option. +#[derive(Resource)] +struct ColoredMesh2dShader(Handle); /// Our custom pipeline needs its own instance storage #[derive(Resource, Deref, DerefMut, Default)] @@ -297,15 +294,16 @@ impl Plugin for ColoredMesh2dPlugin { fn build(&self, app: &mut App) { // Load our custom shader let mut shaders = app.world_mut().resource_mut::>(); - shaders.insert( - &COLORED_MESH2D_SHADER_HANDLE, - Shader::from_wgsl(COLORED_MESH2D_SHADER, file!()), - ); + // Here, we construct and add the shader asset manually. There are many ways to load this + // shader, including `embedded_asset`/`load_embedded_asset`. + let shader = shaders.add(Shader::from_wgsl(COLORED_MESH2D_SHADER, file!())); + app.add_plugins(SyncComponentPlugin::::default()); // Register our custom draw function, and add our render systems app.get_sub_app_mut(RenderApp) .unwrap() + .insert_resource(ColoredMesh2dShader(shader)) .add_render_command::() .init_resource::>() .init_resource::() diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index c549134419..9cffb8e00c 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -129,7 +129,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res) { cmd.with_children(|builder| { builder.spawn(( Text2d::new(rect.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, @@ -275,7 +275,7 @@ fn setup_texture_atlas( cmd.with_children(|builder| { builder.spawn(( Text2d::new(sprite_sheet.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 94f4fe809f..91918b1d66 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -94,7 +94,7 @@ fn spawn_sprites( children![( Text2d::new(label), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), bevy::sprite::Anchor::TOP_CENTER, )], diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 7b1abfd8da..3123e8d891 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 50.0, ..default() }; - let text_justification = JustifyText::Center; + let text_justification = Justify::Center; commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( @@ -78,7 +78,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(Unicode linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::WordBoundary), + TextLayout::new(Justify::Left, LineBreak::WordBoundary), // Wrap text in the rectangle TextBounds::from(box_size), // Ensure the text is drawn on top of the box @@ -94,7 +94,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::AnyCharacter), + TextLayout::new(Justify::Left, LineBreak::AnyCharacter), // Wrap text in the rectangle TextBounds::from(other_box_size), // Ensure the text is drawn on top of the box @@ -104,11 +104,11 @@ fn setup(mut commands: Commands, asset_server: Res) { // Demonstrate font smoothing off commands.spawn(( - Text2d::new("This text has\nFontSmoothing::None\nAnd JustifyText::Center"), + Text2d::new("This text has\nFontSmoothing::None\nAnd Justify::Center"), slightly_smaller_text_font .clone() .with_font_smoothing(FontSmoothing::None), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)), )); diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 7510afbcee..25106adcfb 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -279,7 +279,7 @@ fn create_label( commands.spawn(( Text2d::new(text), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform { translation: Vec3::new(translation.0, translation.1, translation.2), ..default() 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::add] on the shape, which works because the [`Assets::add`] method takes anything that can be turned into the asset type it stores. There's an implementation for [`From`] on shape primitives into [`Mesh`], so that will get called internally by [`Assets::add`]. +//! +//! [`Extrusion`] lets us turn 2D shape primitives into versions of those shapes that have volume by extruding them. A 1x1 square that gets wrapped in this with an extrusion depth of 2 will give us a rectangular prism of size 1x1x2, but here we're just extruding these 2d shapes by depth 1. +//! +//! The material applied to these shapes is a texture that we generate at run time by looping through a "palette" of RGBA values (stored adjacent to each other in the array) and writing values to positions in another array that represents the buffer for an 8x8 texture. This texture is then registered with the assets system just one time, with that [`Handle`] then applied to all the shapes in this example. +//! +//! The mesh and material are [`Handle`] and [`Handle`] at the moment, neither of which implement `Component` on their own. Handles are put behind "newtypes" to prevent ambiguity, as some entities might want to have handles to meshes (or images, or materials etc.) for different purposes! All we need to do to make them rendering-relevant components is wrap the mesh handle and the material handle in [`Mesh3d`] and [`MeshMaterial3d`] respectively. //! //! You can toggle wireframes with the space bar except on wasm. Wasm does not support //! `POLYGON_MODE_LINE` on the gpu. diff --git a/examples/3d/anisotropy.rs b/examples/3d/anisotropy.rs index 371612b2d2..edb0e92539 100644 --- a/examples/3d/anisotropy.rs +++ b/examples/3d/anisotropy.rs @@ -353,11 +353,7 @@ impl AppStatus { let mesh_help_text = format!("Press Q to change to {}", self.visible_scene.next()); // Build the `Text` object. - format!( - "{}\n{}\n{}", - material_variant_help_text, light_help_text, mesh_help_text, - ) - .into() + format!("{material_variant_help_text}\n{light_help_text}\n{mesh_help_text}",).into() } } diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index e29574588c..fd93625c0e 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -5,24 +5,25 @@ use std::{f32::consts::PI, fmt::Write}; use bevy::{ anti_aliasing::{ contrast_adaptive_sharpening::ContrastAdaptiveSharpening, - experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}, fxaa::{Fxaa, Sensitivity}, smaa::{Smaa, SmaaPreset}, + taa::TemporalAntiAliasing, }, core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass}, image::{ImageSampler, ImageSamplerDescriptor}, pbr::CascadeShadowConfigBuilder, prelude::*, render::{ - camera::TemporalJitter, + camera::{MipBias, TemporalJitter}, render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, + view::Hdr, }, }; fn main() { App::new() - .add_plugins((DefaultPlugins, TemporalAntiAliasPlugin)) + .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (modify_aa, modify_sharpening, update_ui)) .run(); @@ -31,6 +32,7 @@ fn main() { type TaaComponents = ( TemporalAntiAliasing, TemporalJitter, + MipBias, DepthPrepass, MotionVectorPrepass, ); @@ -300,10 +302,7 @@ fn setup( // Camera commands.spawn(( Camera3d::default(), - Camera { - hdr: true, - ..default() - }, + Hdr, Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), ContrastAdaptiveSharpening { enabled: false, diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs index 53c5c91dfa..edc6d04dab 100644 --- a/examples/3d/atmosphere.rs +++ b/examples/3d/atmosphere.rs @@ -20,11 +20,6 @@ fn main() { fn setup_camera_fog(mut commands: Commands) { commands.spawn(( Camera3d::default(), - // HDR is required for atmospheric scattering to be properly applied to the scene - Camera { - hdr: true, - ..default() - }, Transform::from_xyz(-1.2, 0.15, 0.0).looking_at(Vec3::Y * 0.1, Vec3::Y), // This is the component that enables atmospheric scattering for a camera Atmosphere::EARTH, @@ -36,7 +31,7 @@ fn setup_camera_fog(mut commands: Commands) { scene_units_to_m: 1e+4, ..Default::default() }, - // The directional light illuminance used in this scene + // The directional light illuminance used in this scene // (the one recommended for use with this feature) is // quite bright, so raising the exposure compensation helps // bring the scene to a nicer brightness range. diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 79fece61c8..62c875dc5d 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -40,10 +40,6 @@ fn setup( commands.spawn(( Camera3d::default(), - Camera { - hdr: true, - ..default() - }, Transform::from_xyz(1.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), AutoExposure { metering_mask: metering_mask.clone(), diff --git a/examples/3d/blend_modes.rs b/examples/3d/blend_modes.rs index 830acfdb34..95fe522cf0 100644 --- a/examples/3d/blend_modes.rs +++ b/examples/3d/blend_modes.rs @@ -10,7 +10,7 @@ //! | `Spacebar` | Toggle Unlit | //! | `C` | Randomize Colors | -use bevy::{color::palettes::css::ORANGE, prelude::*}; +use bevy::{color::palettes::css::ORANGE, prelude::*, render::view::Hdr}; use rand::random; fn main() { @@ -149,6 +149,7 @@ fn setup( commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 2.5, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + Hdr, // Unfortunately, MSAA and HDR are not supported simultaneously under WebGL. // Since this example uses HDR, we must disable MSAA for Wasm builds, at least // until WebGPU is ready and no longer behind a feature flag in Web browsers. @@ -249,13 +250,23 @@ impl Default for ExampleState { fn example_control_system( mut materials: ResMut>, controllable: Query<(&MeshMaterial3d, &ExampleControls)>, - camera: Single<(&mut Camera, &mut Transform, &GlobalTransform), With>, + camera: Single< + ( + Entity, + &mut Camera, + &mut Transform, + &GlobalTransform, + Has, + ), + With, + >, mut labels: Query<(&mut Node, &ExampleLabel)>, mut display: Single<&mut Text, With>, labeled: Query<&GlobalTransform>, mut state: Local, 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, @@ -160,6 +160,6 @@ fn send_and_receive_manual_event_reader( for mut event in events_to_resend { event.times_sent += 1; - events.send(event); + events.write(event); } } diff --git a/examples/ecs/state_scoped.rs b/examples/ecs/state_scoped.rs index e0844b119d..52a01c4d78 100644 --- a/examples/ecs/state_scoped.rs +++ b/examples/ecs/state_scoped.rs @@ -10,7 +10,6 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .init_state::() - .enable_state_scoped_entities::() .add_systems(Startup, setup_camera) .add_systems(OnEnter(GameState::A), on_a_enter) .add_systems(OnEnter(GameState::B), on_b_enter) @@ -117,7 +116,7 @@ fn toggle( state: Res>, mut next_state: ResMut>, ) { - if !timer.0.tick(time.delta()).finished() { + if !timer.0.tick(time.delta()).is_finished() { return; } *next_state = match state.get() { diff --git a/examples/games/alien_cake_addict.rs b/examples/games/alien_cake_addict.rs index 5051c390f8..d0aa9c8680 100644 --- a/examples/games/alien_cake_addict.rs +++ b/examples/games/alien_cake_addict.rs @@ -25,7 +25,6 @@ fn main() { TimerMode::Repeating, ))) .init_state::() - .enable_state_scoped_entities::() .add_systems(Startup, setup_cameras) .add_systems(OnEnter(GameState::Playing), setup) .add_systems( @@ -204,7 +203,7 @@ fn move_player( mut transforms: Query<&mut Transform>, time: Res

for SetWireframe3dPushConstants { pub type DrawWireframe3d = ( SetItemPipeline, SetMeshViewBindGroup<0>, - SetMeshBindGroup<1>, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, SetWireframe3dPushConstants, DrawMesh, ); @@ -341,7 +335,7 @@ impl FromWorld for Wireframe3dPipeline { fn from_world(render_world: &mut World) -> Self { Wireframe3dPipeline { mesh_pipeline: render_world.resource::().clone(), - shader: WIREFRAME_SHADER_HANDLE, + shader: load_embedded_asset!(render_world, "render/wireframe.wgsl"), } } } @@ -382,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::>() @@ -482,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 f02e5237aa..9053f48188 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" @@ -13,20 +13,19 @@ 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_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 3758816ac9..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. @@ -84,7 +84,7 @@ pub struct PointerHits { } impl PointerHits { - #[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + /// Construct [`PointerHits`]. pub fn new(pointer: prelude::PointerId, picks: Vec<(Entity, HitData)>, order: f32) -> Self { Self { pointer, @@ -114,7 +114,7 @@ pub struct HitData { } impl HitData { - #[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + /// Construct a [`HitData`]. pub fn new(camera: Entity, depth: f32, position: Option, normal: Option) -> Self { Self { camera, diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 29405099f6..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. @@ -208,6 +197,11 @@ pub struct Move { /// Information about the picking intersection. pub hit: HitData, /// The change in position since the last move event. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub delta: Vec2, } @@ -228,8 +222,18 @@ pub struct Drag { /// Pointer button pressed and moved to trigger this event. pub button: PointerButton, /// The total distance vector of a drag, measured from drag start to the current position. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub distance: Vec2, /// The change in position since the last drag event. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub delta: Vec2, } @@ -240,6 +244,11 @@ pub struct DragEnd { /// Pointer button pressed, moved, and released to trigger this event. pub button: PointerButton, /// The vector of drag movement measured from start to final pointer position. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub distance: Vec2, } @@ -296,8 +305,20 @@ pub struct DragDrop { #[reflect(Clone, PartialEq)] pub struct DragEntry { /// The position of the pointer at drag start. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using [`Camera::viewport_to_world`](bevy_render::camera::Camera::viewport_to_world) or + /// [`Camera::viewport_to_world_2d`](bevy_render::camera::Camera::viewport_to_world_2d) to + /// convert from screen-space to world-space. pub start_pos: Vec2, /// The latest position of the pointer during this drag, used to compute deltas. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using [`Camera::viewport_to_world`](bevy_render::camera::Camera::viewport_to_world) or + /// [`Camera::viewport_to_world_2d`](bevy_render::camera::Camera::viewport_to_world_2d) to + /// convert from screen-space to world-space. pub latest_pos: Vec2, } @@ -368,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>, @@ -380,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. @@ -390,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 @@ -398,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: @@ -420,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 @@ -473,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); @@ -490,7 +506,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragLeave { button, dragged: *drag_target, @@ -527,16 +542,11 @@ pub fn pointer_events( for button in PointerButton::iter() { let state = pointer_state.get_mut(pointer_id, button); - for drag_target in state - .dragging - .keys() - .filter(|&&drag_target| hovered_entity != drag_target) - { + for drag_target in state.dragging.keys() { state.dragging_over.insert(hovered_entity, hit.clone()); let drag_enter_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragEnter { button, dragged: *drag_target, @@ -549,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); } @@ -580,8 +585,7 @@ pub fn pointer_events( let pressed_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, - Pressed { + Press { button, hit: hit.clone(), }, @@ -608,7 +612,6 @@ pub fn pointer_events( let click_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Click { button, hit: hit.clone(), @@ -618,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(), }, @@ -639,7 +641,6 @@ pub fn pointer_events( let drag_drop_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragDrop { button, dropped: drag_target, @@ -653,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, @@ -666,7 +666,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragLeave { button, dragged: drag_target, @@ -707,7 +706,6 @@ pub fn pointer_events( let drag_start_event = Pointer::new( pointer_id, location.clone(), - *press_target, DragStart { button, hit: hit.clone(), @@ -726,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, @@ -749,7 +746,6 @@ pub fn pointer_events( let drag_over_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragOver { button, dragged: *drag_target, @@ -771,7 +767,6 @@ pub fn pointer_events( let move_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Move { hit: hit.clone(), delta, @@ -791,7 +786,6 @@ pub fn pointer_events( let scroll_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Scroll { unit, x, @@ -811,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 6347568c02..f675568394 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::*; @@ -208,18 +208,6 @@ pub fn update_interactions( mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>, mut interact: Query<&mut PickingInteraction>, ) { - // Clear all previous hover data from pointers and entities - for (pointer, _, mut pointer_interaction) in &mut pointers { - pointer_interaction.sorted_entities.clear(); - if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) { - for entity in previously_hovered_entities.keys() { - if let Ok(mut interaction) = interact.get_mut(*entity) { - *interaction = PickingInteraction::None; - } - } - } - } - // Create a map to hold the aggregated interaction for each entity. This is needed because we // need to be able to insert the interaction component on entities if they do not exist. To do // so we need to know the final aggregated interaction state to avoid the scenario where we set @@ -239,13 +227,29 @@ pub fn update_interactions( } // Take the aggregated entity states and update or insert the component if missing. - for (hovered_entity, new_interaction) in new_interaction_state.drain() { + for (&hovered_entity, &new_interaction) in new_interaction_state.iter() { if let Ok(mut interaction) = interact.get_mut(hovered_entity) { - *interaction = new_interaction; + interaction.set_if_neq(new_interaction); } else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) { entity_commands.try_insert(new_interaction); } } + + // Clear all previous hover data from pointers that are no longer hovering any entities. + // We do this last to preserve change detection for picking interactions. + for (pointer, _, _) in &mut pointers { + let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else { + continue; + }; + + for entity in previously_hovered_entities.keys() { + if !new_interaction_state.contains_key(entity) { + if let Ok(mut interaction) = interact.get_mut(*entity) { + interaction.set_if_neq(PickingInteraction::None); + } + } + } + } } /// Merge the interaction state of this entity into the aggregated map. @@ -275,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 descendants 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/input.rs b/crates/bevy_picking/src/input.rs index d751e07c94..3050522ab5 100644 --- a/crates/bevy_picking/src/input.rs +++ b/crates/bevy_picking/src/input.rs @@ -22,7 +22,7 @@ use bevy_input::{ use bevy_math::Vec2; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::prelude::*; -use bevy_render::camera::RenderTarget; +use bevy_render::camera::{RenderTarget, ToNormalizedRenderTarget as _}; use bevy_window::{PrimaryWindow, WindowEvent, WindowRef}; use tracing::debug; @@ -39,24 +39,30 @@ pub mod prelude { pub use crate::input::PointerInputPlugin; } -/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin, -/// that you can replace with your own plugin as needed. -/// -/// [`crate::PickingPlugin::is_input_enabled`] can be used to toggle whether -/// the core picking plugin processes the inputs sent by this, or other input plugins, in one place. -/// -/// This plugin contains several settings, and is added to the world as a resource after initialization. -/// You can configure pointer input settings at runtime by accessing the resource. #[derive(Copy, Clone, Resource, Debug, Reflect)] #[reflect(Resource, Default, Clone)] -pub struct PointerInputPlugin { +/// Settings for enabling and disabling updating mouse and touch inputs for picking +/// +/// ## Custom initialization +/// ``` +/// # use bevy_app::App; +/// # use bevy_picking::input::{PointerInputSettings,PointerInputPlugin}; +/// App::new() +/// .insert_resource(PointerInputSettings { +/// is_touch_enabled: false, +/// is_mouse_enabled: true, +/// }) +/// // or DefaultPlugins +/// .add_plugins(PointerInputPlugin); +/// ``` +pub struct PointerInputSettings { /// Should touch inputs be updated? pub is_touch_enabled: bool, /// Should mouse inputs be updated? pub is_mouse_enabled: bool, } -impl PointerInputPlugin { +impl PointerInputSettings { fn is_mouse_enabled(state: Res) -> bool { state.is_mouse_enabled } @@ -66,7 +72,7 @@ impl PointerInputPlugin { } } -impl Default for PointerInputPlugin { +impl Default for PointerInputSettings { fn default() -> Self { Self { is_touch_enabled: true, @@ -75,25 +81,35 @@ impl Default for PointerInputPlugin { } } +/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin, +/// that you can replace with your own plugin as needed. +/// +/// Toggling mouse input or touch input can be done at runtime by modifying +/// [`PointerInputSettings`] resource. +/// +/// [`PointerInputSettings`] can be initialized with custom values, but will be +/// initialized with default values if it is not present at the moment this is +/// added to the app. +pub struct PointerInputPlugin; + impl Plugin for PointerInputPlugin { fn build(&self, app: &mut App) { - app.insert_resource(*self) + app.init_resource::() + .register_type::() .add_systems(Startup, spawn_mouse_pointer) .add_systems( First, ( - mouse_pick_events.run_if(PointerInputPlugin::is_mouse_enabled), - touch_pick_events.run_if(PointerInputPlugin::is_touch_enabled), + mouse_pick_events.run_if(PointerInputSettings::is_mouse_enabled), + touch_pick_events.run_if(PointerInputSettings::is_touch_enabled), ) .chain() .in_set(PickingSystems::Input), ) .add_systems( Last, - deactivate_touch_pointers.run_if(PointerInputPlugin::is_touch_enabled), - ) - .register_type::() - .register_type::(); + deactivate_touch_pointers.run_if(PointerInputSettings::is_touch_enabled), + ); } } diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 53387e84c8..026d2a1953 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>| { +//! .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| { +//! .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,27 +286,38 @@ 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; impl PluginGroup for DefaultPickingPlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() - .add(input::PointerInputPlugin::default()) - .add(PickingPlugin::default()) + .add(input::PointerInputPlugin) + .add(PickingPlugin) .add(InteractionPlugin) } } -/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared -/// types used by other picking plugins. -/// -/// This plugin contains several settings, and is added to the world as a resource after initialization. You -/// can configure picking settings at runtime through the resource. #[derive(Copy, Clone, Debug, Resource, Reflect)] #[reflect(Resource, Default, Debug, Clone)] -pub struct PickingPlugin { +/// Controls the behavior of picking +/// +/// ## Custom initialization +/// ``` +/// # use bevy_app::App; +/// # use bevy_picking::{PickingSettings, PickingPlugin}; +/// App::new() +/// .insert_resource(PickingSettings { +/// is_enabled: true, +/// is_input_enabled: false, +/// is_hover_enabled: true, +/// is_window_picking_enabled: false, +/// }) +/// // or DefaultPlugins +/// .add_plugins(PickingPlugin); +/// ``` +pub struct PickingSettings { /// Enables and disables all picking features. pub is_enabled: bool, /// Enables and disables input collection. @@ -316,7 +328,7 @@ pub struct PickingPlugin { pub is_window_picking_enabled: bool, } -impl PickingPlugin { +impl PickingSettings { /// Whether or not input collection systems should be running. pub fn input_should_run(state: Res) -> bool { state.is_input_enabled && state.is_enabled @@ -334,7 +346,7 @@ impl PickingPlugin { } } -impl Default for PickingPlugin { +impl Default for PickingSettings { fn default() -> Self { Self { is_enabled: true, @@ -345,9 +357,18 @@ impl Default for PickingPlugin { } } +/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared +/// types used by other picking plugins. +/// +/// Behavior of picking can be controlled by modifying [`PickingSettings`]. +/// +/// [`PickingSettings`] will be initialized with default values if it +/// is not present at the moment this is added to the app. +pub struct PickingPlugin; + impl Plugin for PickingPlugin { fn build(&self, app: &mut App) { - app.insert_resource(*self) + app.init_resource::() .init_resource::() .init_resource::() .add_event::() @@ -368,7 +389,7 @@ impl Plugin for PickingPlugin { .add_systems( PreUpdate, window::update_window_hits - .run_if(Self::window_picking_should_run) + .run_if(PickingSettings::window_picking_should_run) .in_set(PickingSystems::Backend), ) .configure_sets( @@ -381,17 +402,18 @@ impl Plugin for PickingPlugin { .configure_sets( PreUpdate, ( - PickingSystems::ProcessInput.run_if(Self::input_should_run), + PickingSystems::ProcessInput.run_if(PickingSettings::input_should_run), PickingSystems::Backend, - PickingSystems::Hover.run_if(Self::hover_should_run), + PickingSystems::Hover.run_if(PickingSettings::hover_should_run), PickingSystems::PostHover, PickingSystems::Last, ) .chain(), ) - .register_type::() + .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -414,7 +436,7 @@ impl Plugin for InteractionPlugin { .init_resource::() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_event::>() .add_event::>() @@ -425,11 +447,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/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index fe08976eb8..8e6a16690c 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -1,7 +1,7 @@ //! A [mesh ray casting](ray_cast) backend for [`bevy_picking`](crate). //! -//! By default, all meshes are pickable. Picking can be disabled for individual entities -//! by adding [`Pickable::IGNORE`]. +//! By default, all meshes that have [`bevy_asset::RenderAssetUsages::MAIN_WORLD`] are pickable. +//! Picking can be disabled for individual entities by adding [`Pickable::IGNORE`]. //! //! To make mesh picking entirely opt-in, set [`MeshPickingSettings::require_markers`] //! to `true` and add [`MeshPickingCamera`] and [`Pickable`] components to the desired camera and 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 e180a9c1be..95a44ab3ec 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -13,7 +13,7 @@ use bevy_input::mouse::MouseScrollUnit; use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::prelude::*; -use bevy_render::camera::{Camera, NormalizedRenderTarget}; +use bevy_render::camera::{Camera, NormalizedRenderTarget, ToNormalizedRenderTarget as _}; use bevy_window::PrimaryWindow; use uuid::Uuid; @@ -205,8 +205,8 @@ impl PointerLocation { /// - a pointer is not associated with a [`Camera`] because multiple cameras can target the same /// render target. It is up to picking backends to associate a Pointer's `Location` with a /// specific `Camera`, if any. -#[derive(Debug, Clone, Component, Reflect, PartialEq)] -#[reflect(Component, Debug, PartialEq, Clone)] +#[derive(Debug, Clone, Reflect, PartialEq)] +#[reflect(Debug, PartialEq, Clone)] pub struct Location { /// The [`NormalizedRenderTarget`] associated with the pointer, usually a window. pub target: NormalizedRenderTarget, @@ -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 44c680394d..614ff15fce 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] diff --git a/crates/bevy_platform/src/cell/mod.rs b/crates/bevy_platform/src/cell/mod.rs new file mode 100644 index 0000000000..04f8bf6572 --- /dev/null +++ b/crates/bevy_platform/src/cell/mod.rs @@ -0,0 +1,9 @@ +//! Provides cell primitives. +//! +//! This is a drop-in replacement for `std::cell::SyncCell`/`std::cell::SyncUnsafeCell`. + +mod sync_cell; +mod sync_unsafe_cell; + +pub use sync_cell::SyncCell; +pub use sync_unsafe_cell::SyncUnsafeCell; diff --git a/crates/bevy_utils/src/synccell.rs b/crates/bevy_platform/src/cell/sync_cell.rs similarity index 100% rename from crates/bevy_utils/src/synccell.rs rename to crates/bevy_platform/src/cell/sync_cell.rs diff --git a/crates/bevy_utils/src/syncunsafecell.rs b/crates/bevy_platform/src/cell/sync_unsafe_cell.rs similarity index 98% rename from crates/bevy_utils/src/syncunsafecell.rs rename to crates/bevy_platform/src/cell/sync_unsafe_cell.rs index 104256969d..bb67557ec2 100644 --- a/crates/bevy_utils/src/syncunsafecell.rs +++ b/crates/bevy_platform/src/cell/sync_unsafe_cell.rs @@ -94,7 +94,7 @@ impl SyncUnsafeCell<[T]> { /// # Examples /// /// ``` - /// # use bevy_utils::syncunsafecell::SyncUnsafeCell; + /// # use bevy_platform::cell::SyncUnsafeCell; /// /// let slice: &mut [i32] = &mut [1, 2, 3]; /// let cell_slice: &SyncUnsafeCell<[i32]> = SyncUnsafeCell::from_mut(slice); diff --git a/crates/bevy_platform/src/lib.rs b/crates/bevy_platform/src/lib.rs index 668442f299..0dac76b011 100644 --- a/crates/bevy_platform/src/lib.rs +++ b/crates/bevy_platform/src/lib.rs @@ -1,13 +1,13 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] //! Platform compatibility support for first-party [Bevy] engine crates. //! -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ cfg::std! { extern crate std; @@ -19,6 +19,7 @@ cfg::alloc! { pub mod collections; } +pub mod cell; pub mod cfg; pub mod hash; pub mod sync; diff --git a/crates/bevy_platform/src/thread.rs b/crates/bevy_platform/src/thread.rs index 6e4650382e..7fc7413bc6 100644 --- a/crates/bevy_platform/src/thread.rs +++ b/crates/bevy_platform/src/thread.rs @@ -21,7 +21,7 @@ crate::cfg::switch! { let start = Instant::now(); while start.elapsed() < dur { - spin_loop() + spin_loop(); } } } diff --git a/crates/bevy_platform/src/time/fallback.rs b/crates/bevy_platform/src/time/fallback.rs index c649f6a49d..2964c9d980 100644 --- a/crates/bevy_platform/src/time/fallback.rs +++ b/crates/bevy_platform/src/time/fallback.rs @@ -155,14 +155,14 @@ fn unset_getter() -> Duration { let nanos = unsafe { core::arch::x86::_rdtsc() }; - return Duration::from_nanos(nanos); + Duration::from_nanos(nanos) } #[cfg(target_arch = "x86_64")] => { // SAFETY: standard technique for getting a nanosecond counter on x86_64 let nanos = unsafe { core::arch::x86_64::_rdtsc() }; - return Duration::from_nanos(nanos); + Duration::from_nanos(nanos) } #[cfg(target_arch = "aarch64")] => { // SAFETY: standard technique for getting a nanosecond counter of aarch64 @@ -171,7 +171,7 @@ fn unset_getter() -> Duration { core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks); ticks }; - return Duration::from_nanos(nanos); + Duration::from_nanos(nanos) } _ => { panic!("An elapsed time getter has not been provided to `Instant`. Please use `Instant::set_elapsed(...)` before calling `Instant::now()`") diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index 0f56880bd4..07c6eeae68 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "no_std"] diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 1580f3f926..15a193d737 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -3,8 +3,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![expect(unsafe_code, reason = "Raw pointers are inherently unsafe.")] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] use core::{ @@ -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 bb72226ab8..8e2d4d0f38 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_reflect" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Dynamically interact with rust types" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -54,7 +54,6 @@ wgpu-types = ["dep:wgpu-types"] ## on `no_std` targets, but provides access to certain additional features on ## supported platforms. std = [ - "bevy_utils/std", "erased-serde/std", "downcast-rs/std", "serde/std", @@ -67,10 +66,7 @@ std = [ ## `critical-section` provides the building blocks for synchronization primitives ## on all platforms, including `no_std`. -critical-section = [ - "bevy_platform/critical-section", - "bevy_utils/critical-section", -] +critical-section = ["bevy_platform/critical-section"] ## Enables use of browser APIs. ## Note this is currently only applicable on `wasm32` architectures. @@ -78,12 +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, features = [ - "alloc", -] } -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", ] } @@ -99,14 +93,14 @@ 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 } +smallvec = { version = "1", default-features = false, optional = true } glam = { version = "0.29.3", default-features = false, features = [ "serde", ], optional = true } -petgraph = { version = "0.7", features = ["serde-1"], optional = true } +petgraph = { version = "0.8", features = ["serde-1"], optional = true } smol_str = { version = "0.2.0", default-features = false, features = [ "serde", ], optional = true } @@ -115,15 +109,15 @@ 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" +serde_json = "1.0.140" serde = { version = "1", features = ["derive"] } static_assertions = "1.1.0" diff --git a/crates/bevy_reflect/compile_fail/Cargo.toml b/crates/bevy_reflect/compile_fail/Cargo.toml index 178711c5d0..e3cb14ec2d 100644 --- a/crates/bevy_reflect/compile_fail/Cargo.toml +++ b/crates/bevy_reflect/compile_fail/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_reflect_compile_fail" edition = "2024" description = "Compile fail tests for Bevy Engine's reflection system" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" publish = false diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_fail.rs index d691c824cc..67135669f5 100644 --- a/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_fail.rs +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_fail.rs @@ -9,6 +9,7 @@ mod structs { #[reflect_remote(external_crate::TheirStruct)] //~^ ERROR: `?` operator has incompatible types + //~| ERROR: mismatched types struct MyStruct { // Reason: Should be `u32` pub value: bool, @@ -25,6 +26,7 @@ mod tuple_structs { #[reflect_remote(external_crate::TheirStruct)] //~^ ERROR: `?` operator has incompatible types + //~| ERROR: mismatched types struct MyStruct( // Reason: Should be `u32` pub bool, @@ -48,6 +50,7 @@ mod enums { //~| ERROR: variant `enums::external_crate::TheirStruct::Unit` has no field named `0` //~| ERROR: `?` operator has incompatible types //~| ERROR: `?` operator has incompatible types + //~| ERROR: mismatched types enum MyStruct { // Reason: Should be unit variant Unit(i32), @@ -57,6 +60,7 @@ mod enums { // Reason: Should be `usize` Struct { value: String }, //~^ ERROR: mismatched types + //~| ERROR: mismatched types } } diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_fail.rs index 457f1f75e5..391258ccc6 100644 --- a/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_fail.rs +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_fail.rs @@ -26,8 +26,8 @@ mod incorrect_inner_type { //~| ERROR: `TheirInner` does not implement `PartialReflect` so cannot be introspected //~| ERROR: `TheirInner` does not implement `PartialReflect` so cannot be introspected //~| ERROR: `TheirInner` does not implement `TypePath` so cannot provide dynamic type path information - //~| ERROR: `TheirInner` does not implement `TypePath` so cannot provide dynamic type path information //~| ERROR: `?` operator has incompatible types + //~| ERROR: mismatched types struct MyOuter { // Reason: Should not use `MyInner` directly pub inner: MyInner, diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_fail.rs index 1983442ab7..e3c894e6d5 100644 --- a/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_fail.rs +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_fail.rs @@ -77,6 +77,7 @@ mod enums { #[reflect_remote(external_crate::TheirBar)] //~^ ERROR: `?` operator has incompatible types + //~| ERROR: mismatched types enum MyBar { // Reason: Should use `i32` Value(u32), diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index ad6ec8cd2f..19875633ed 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -19,11 +19,11 @@ 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" } +indexmap = "2.0" proc-macro2 = "1.0" quote = "1.0" -syn = { version = "2.0", features = ["full"] } -uuid = { version = "1.13.1", features = ["v4"] } +syn = { version = "2.0", features = ["full", "extra-traits"] } [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_reflect/derive/src/custom_attributes.rs b/crates/bevy_reflect/derive/src/custom_attributes.rs index f12b6d7c12..1f141f79dc 100644 --- a/crates/bevy_reflect/derive/src/custom_attributes.rs +++ b/crates/bevy_reflect/derive/src/custom_attributes.rs @@ -28,6 +28,11 @@ impl CustomAttributes { Ok(()) } + /// Is the collection empty? + pub fn is_empty(&self) -> bool { + self.attributes.is_empty() + } + /// Parse `@` (custom attribute) attribute. /// /// Examples: diff --git a/crates/bevy_reflect/derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs index f825cb2905..3f6532a408 100644 --- a/crates/bevy_reflect/derive/src/derive_data.rs +++ b/crates/bevy_reflect/derive/src/derive_data.rs @@ -1,4 +1,5 @@ use core::fmt; +use indexmap::IndexSet; use proc_macro2::Span; use crate::{ @@ -481,7 +482,6 @@ impl<'a> ReflectMeta<'a> { where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { crate::registration::impl_get_type_registration( - self, where_clause_options, None, Option::>::None, @@ -514,25 +514,27 @@ impl<'a> StructField<'a> { }; let ty = self.reflected_type(); - let custom_attributes = self.attrs.custom_attributes.to_tokens(bevy_reflect_path); - #[cfg_attr( - not(feature = "documentation"), - expect( - unused_mut, - reason = "Needs to be mutable if `documentation` feature is enabled.", - ) - )] let mut info = quote! { - #field_info::new::<#ty>(#name).with_custom_attributes(#custom_attributes) + #field_info::new::<#ty>(#name) }; + let custom_attributes = &self.attrs.custom_attributes; + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + #[cfg(feature = "documentation")] { let docs = &self.doc; - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } info @@ -596,7 +598,6 @@ impl<'a> ReflectStruct<'a> { where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { crate::registration::impl_get_type_registration( - self.meta(), where_clause_options, self.serialization_data(), Some(self.active_types().iter()), @@ -605,9 +606,12 @@ impl<'a> ReflectStruct<'a> { /// Get a collection of types which are exposed to the reflection API pub fn active_types(&self) -> Vec { + // Collect via `IndexSet` to eliminate duplicate types. self.active_fields() .map(|field| field.reflected_type().clone()) - .collect() + .collect::>() + .into_iter() + .collect::>() } /// Get an iterator of fields which are exposed to the reflection API. @@ -653,19 +657,20 @@ impl<'a> ReflectStruct<'a> { .active_fields() .map(|field| field.to_info_tokens(bevy_reflect_path)); - let custom_attributes = self - .meta - .attrs - .custom_attributes() - .to_tokens(bevy_reflect_path); - let mut info = quote! { #bevy_reflect_path::#info_struct::new::(&[ #(#field_infos),* ]) - .with_custom_attributes(#custom_attributes) }; + let custom_attributes = self.meta.attrs.custom_attributes(); + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + if let Some(generics) = generate_generics(self.meta()) { info.extend(quote! { .with_generics(#generics) @@ -675,9 +680,11 @@ impl<'a> ReflectStruct<'a> { #[cfg(feature = "documentation")] { let docs = self.meta().doc(); - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } quote! { @@ -715,18 +722,7 @@ impl<'a> ReflectStruct<'a> { } } else { quote! { - #bevy_reflect_path::PartialReflect::reflect_clone(#accessor)? - .take() - .map_err(|value| #bevy_reflect_path::ReflectCloneError::FailedDowncast { - expected: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed( - <#field_ty as #bevy_reflect_path::TypePath>::type_path() - ), - received: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Owned( - #bevy_reflect_path::__macro_exports::alloc_utils::ToString::to_string( - #bevy_reflect_path::DynamicTypePath::reflect_type_path(&*value) - ) - ), - })? + <#field_ty as #bevy_reflect_path::PartialReflect>::reflect_clone_and_take(#accessor)? } }; @@ -846,9 +842,12 @@ impl<'a> ReflectEnum<'a> { /// Get a collection of types which are exposed to the reflection API pub fn active_types(&self) -> Vec { + // Collect via `IndexSet` to eliminate duplicate types. self.active_fields() .map(|field| field.reflected_type().clone()) - .collect() + .collect::>() + .into_iter() + .collect::>() } /// Get an iterator of fields which are exposed to the reflection API @@ -868,7 +867,6 @@ impl<'a> ReflectEnum<'a> { where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { crate::registration::impl_get_type_registration( - self.meta(), where_clause_options, None, Some(self.active_fields().map(StructField::reflected_type)), @@ -884,19 +882,20 @@ impl<'a> ReflectEnum<'a> { .iter() .map(|variant| variant.to_info_tokens(bevy_reflect_path)); - let custom_attributes = self - .meta - .attrs - .custom_attributes() - .to_tokens(bevy_reflect_path); - let mut info = quote! { #bevy_reflect_path::EnumInfo::new::(&[ #(#variants),* ]) - .with_custom_attributes(#custom_attributes) }; + let custom_attributes = self.meta.attrs.custom_attributes(); + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + if let Some(generics) = generate_generics(self.meta()) { info.extend(quote! { .with_generics(#generics) @@ -906,9 +905,11 @@ impl<'a> ReflectEnum<'a> { #[cfg(feature = "documentation")] { let docs = self.meta().doc(); - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } quote! { @@ -1008,26 +1009,26 @@ impl<'a> EnumVariant<'a> { } }; - let custom_attributes = self.attrs.custom_attributes.to_tokens(bevy_reflect_path); - - #[cfg_attr( - not(feature = "documentation"), - expect( - unused_mut, - reason = "Needs to be mutable if `documentation` feature is enabled.", - ) - )] let mut info = quote! { #bevy_reflect_path::#info_struct::new(#args) - .with_custom_attributes(#custom_attributes) }; + let custom_attributes = &self.attrs.custom_attributes; + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + #[cfg(feature = "documentation")] { let docs = &self.doc; - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } quote! { diff --git a/crates/bevy_reflect/derive/src/documentation.rs b/crates/bevy_reflect/derive/src/documentation.rs index 33aec4c4f3..4fbcf775f4 100644 --- a/crates/bevy_reflect/derive/src/documentation.rs +++ b/crates/bevy_reflect/derive/src/documentation.rs @@ -61,6 +61,11 @@ impl Documentation { ) } + /// Is the collection empty? + pub fn is_empty(&self) -> bool { + self.docs.is_empty() + } + /// Push a new docstring to the collection pub fn push(&mut self, doc: String) { self.docs.push(doc); diff --git a/crates/bevy_reflect/derive/src/enum_utility.rs b/crates/bevy_reflect/derive/src/enum_utility.rs index 5571b861a6..c717b7723e 100644 --- a/crates/bevy_reflect/derive/src/enum_utility.rs +++ b/crates/bevy_reflect/derive/src/enum_utility.rs @@ -48,20 +48,15 @@ pub(crate) trait VariantBuilder: Sized { /// * `this`: The identifier of the enum /// * `field`: The field to access fn access_field(&self, this: &Ident, field: VariantField) -> TokenStream { - match &field.field.data.ident { - Some(field_ident) => { - let name = field_ident.to_string(); - quote!(#this.field(#name)) - } - None => { - if let Some(field_index) = field.field.reflection_index { - quote!(#this.field_at(#field_index)) - } else { - quote!(::core::compile_error!( - "internal bevy_reflect error: field should be active" - )) - } - } + if let Some(field_ident) = &field.field.data.ident { + let name = field_ident.to_string(); + quote!(#this.field(#name)) + } else if let Some(field_index) = field.field.reflection_index { + quote!(#this.field_at(#field_index)) + } else { + quote!(::core::compile_error!( + "internal bevy_reflect error: field should be active" + )) } } @@ -321,9 +316,7 @@ impl<'a> VariantBuilder for ReflectCloneVariantBuilder<'a> { fn construct_field(&self, field: VariantField) -> TokenStream { let bevy_reflect_path = self.reflect_enum.meta().bevy_reflect_path(); - let field_ty = field.field.reflected_type(); - let alias = field.alias; let alias = match &field.field.attrs.remote { Some(wrapper_ty) => { @@ -337,18 +330,7 @@ impl<'a> VariantBuilder for ReflectCloneVariantBuilder<'a> { match &field.field.attrs.clone { CloneBehavior::Default => { quote! { - #bevy_reflect_path::PartialReflect::reflect_clone(#alias)? - .take() - .map_err(|value| #bevy_reflect_path::ReflectCloneError::FailedDowncast { - expected: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed( - <#field_ty as #bevy_reflect_path::TypePath>::type_path() - ), - received: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Owned( - #bevy_reflect_path::__macro_exports::alloc_utils::ToString::to_string( - #bevy_reflect_path::DynamicTypePath::reflect_type_path(&*value) - ) - ), - })? + <#field_ty as #bevy_reflect_path::PartialReflect>::reflect_clone_and_take(#alias)? } } CloneBehavior::Trait => { diff --git a/crates/bevy_reflect/derive/src/from_reflect.rs b/crates/bevy_reflect/derive/src/from_reflect.rs index d994cbd2f7..a0e6e444d3 100644 --- a/crates/bevy_reflect/derive/src/from_reflect.rs +++ b/crates/bevy_reflect/derive/src/from_reflect.rs @@ -146,7 +146,8 @@ fn impl_struct_internal( quote! { let mut #__this = <#reflect_ty as #FQDefault>::default(); #( - if let #fqoption::Some(__field) = #active_values() { + // The closure catches any failing `?` within `active_values`. + if let #fqoption::Some(__field) = (|| #active_values)() { // Iff field exists -> use its value #__this.#active_members = __field; } @@ -158,7 +159,7 @@ fn impl_struct_internal( quote! { let #__this = #constructor { - #(#active_members: #active_values()?,)* + #(#active_members: #active_values?,)* #(#ignored_members: #ignored_values,)* }; #FQOption::Some(#retval) @@ -274,13 +275,11 @@ fn get_active_fields( <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) }); quote! { - (|| - if let #FQOption::Some(field) = #get_field { - #value - } else { - #FQOption::Some(#path()) - } - ) + if let #FQOption::Some(field) = #get_field { + #value + } else { + #FQOption::Some(#path()) + } } } DefaultBehavior::Default => { @@ -288,13 +287,11 @@ fn get_active_fields( <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) }); quote! { - (|| - if let #FQOption::Some(field) = #get_field { - #value - } else { - #FQOption::Some(#FQDefault::default()) - } - ) + if let #FQOption::Some(field) = #get_field { + #value + } else { + #FQOption::Some(#FQDefault::default()) + } } } DefaultBehavior::Required => { @@ -302,7 +299,7 @@ fn get_active_fields( <#ty as #bevy_reflect_path::FromReflect>::from_reflect(#get_field?) }); quote! { - (|| #value) + #value } } }; diff --git a/crates/bevy_reflect/derive/src/impls/common.rs b/crates/bevy_reflect/derive/src/impls/common.rs index e8fdadb03e..87836e383d 100644 --- a/crates/bevy_reflect/derive/src/impls/common.rs +++ b/crates/bevy_reflect/derive/src/impls/common.rs @@ -4,10 +4,8 @@ use quote::quote; use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; -pub fn impl_full_reflect( - meta: &ReflectMeta, - where_clause_options: &WhereClauseOptions, -) -> proc_macro2::TokenStream { +pub fn impl_full_reflect(where_clause_options: &WhereClauseOptions) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect_path = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/enums.rs b/crates/bevy_reflect/derive/src/impls/enums.rs index 3cbd8cce95..f2272c7c81 100644 --- a/crates/bevy_reflect/derive/src/impls/enums.rs +++ b/crates/bevy_reflect/derive/src/impls/enums.rs @@ -36,8 +36,6 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream let ref_index = Ident::new("__index_param", Span::call_site()); let ref_value = Ident::new("__value_param", Span::call_site()); - let where_clause_options = reflect_enum.where_clause_options(); - let EnumImpls { enum_field, enum_field_mut, @@ -57,14 +55,11 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream .. } = TryApplyVariantBuilder::new(reflect_enum).build(&ref_value); - let typed_impl = impl_typed( - reflect_enum.meta(), - &where_clause_options, - reflect_enum.to_info_tokens(), - ); + let where_clause_options = reflect_enum.where_clause_options(); + let typed_impl = impl_typed(&where_clause_options, reflect_enum.to_info_tokens()); let type_path_impl = impl_type_path(reflect_enum.meta()); - let full_reflect_impl = impl_full_reflect(reflect_enum.meta(), &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods( reflect_enum.meta(), || Some(quote!(#bevy_reflect_path::enum_partial_eq)), @@ -75,8 +70,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = - crate::impls::impl_function_traits(reflect_enum.meta(), &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let get_type_registration_impl = reflect_enum.get_type_registration(&where_clause_options); diff --git a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs index 77b984e04a..2220a704ea 100644 --- a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs +++ b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs @@ -1,11 +1,9 @@ -use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; +use crate::where_clause_options::WhereClauseOptions; use bevy_macro_utils::fq_std::FQResult; use quote::quote; -pub(crate) fn impl_from_arg( - meta: &ReflectMeta, - where_clause_options: &WhereClauseOptions, -) -> proc_macro2::TokenStream { +pub(crate) fn impl_from_arg(where_clause_options: &WhereClauseOptions) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/func/function_impls.rs b/crates/bevy_reflect/derive/src/impls/func/function_impls.rs index 64ca7ca7b7..acbda3459b 100644 --- a/crates/bevy_reflect/derive/src/impls/func/function_impls.rs +++ b/crates/bevy_reflect/derive/src/impls/func/function_impls.rs @@ -1,5 +1,4 @@ use crate::{ - derive_data::ReflectMeta, impls::func::{ from_arg::impl_from_arg, get_ownership::impl_get_ownership, into_return::impl_into_return, }, @@ -8,12 +7,11 @@ use crate::{ use quote::quote; pub(crate) fn impl_function_traits( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { - let get_ownership = impl_get_ownership(meta, where_clause_options); - let from_arg = impl_from_arg(meta, where_clause_options); - let into_return = impl_into_return(meta, where_clause_options); + let get_ownership = impl_get_ownership(where_clause_options); + let from_arg = impl_from_arg(where_clause_options); + let into_return = impl_into_return(where_clause_options); quote! { #get_ownership diff --git a/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs b/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs index 01d33eb7bb..abdfb803ed 100644 --- a/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs +++ b/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs @@ -1,10 +1,10 @@ -use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; +use crate::where_clause_options::WhereClauseOptions; use quote::quote; pub(crate) fn impl_get_ownership( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/func/into_return.rs b/crates/bevy_reflect/derive/src/impls/func/into_return.rs index f7d1e0b889..221028a99e 100644 --- a/crates/bevy_reflect/derive/src/impls/func/into_return.rs +++ b/crates/bevy_reflect/derive/src/impls/func/into_return.rs @@ -1,10 +1,10 @@ -use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; +use crate::where_clause_options::WhereClauseOptions; use quote::quote; pub(crate) fn impl_into_return( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/opaque.rs b/crates/bevy_reflect/derive/src/impls/opaque.rs index 2a08cadc28..a39b0b4849 100644 --- a/crates/bevy_reflect/derive/src/impls/opaque.rs +++ b/crates/bevy_reflect/derive/src/impls/opaque.rs @@ -21,7 +21,6 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream { let where_clause_options = WhereClauseOptions::new(meta); let typed_impl = impl_typed( - meta, &where_clause_options, quote! { let info = #bevy_reflect_path::OpaqueInfo::new::() #with_docs; @@ -30,7 +29,7 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream { ); let type_path_impl = impl_type_path(meta); - let full_reflect_impl = impl_full_reflect(meta, &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods(meta, || None, || None); let clone_fn = meta.attrs().get_clone_impl(bevy_reflect_path); @@ -54,7 +53,7 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream { #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = crate::impls::impl_function_traits(meta, &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl(); let where_reflect_clause = where_clause_options.extend_where_clause(where_clause); diff --git a/crates/bevy_reflect/derive/src/impls/structs.rs b/crates/bevy_reflect/derive/src/impls/structs.rs index 7e10de3f2b..b78ce40a08 100644 --- a/crates/bevy_reflect/derive/src/impls/structs.rs +++ b/crates/bevy_reflect/derive/src/impls/structs.rs @@ -34,14 +34,10 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS } = FieldAccessors::new(reflect_struct); let where_clause_options = reflect_struct.where_clause_options(); - let typed_impl = impl_typed( - reflect_struct.meta(), - &where_clause_options, - reflect_struct.to_info_tokens(false), - ); + let typed_impl = impl_typed(&where_clause_options, reflect_struct.to_info_tokens(false)); let type_path_impl = impl_type_path(reflect_struct.meta()); - let full_reflect_impl = impl_full_reflect(reflect_struct.meta(), &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods( reflect_struct.meta(), || Some(quote!(#bevy_reflect_path::struct_partial_eq)), @@ -52,8 +48,7 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = - crate::impls::impl_function_traits(reflect_struct.meta(), &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options); diff --git a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs index 90c3555230..01b6a46b7b 100644 --- a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs +++ b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs @@ -24,14 +24,10 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: let where_clause_options = reflect_struct.where_clause_options(); let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options); - let typed_impl = impl_typed( - reflect_struct.meta(), - &where_clause_options, - reflect_struct.to_info_tokens(true), - ); + let typed_impl = impl_typed(&where_clause_options, reflect_struct.to_info_tokens(true)); let type_path_impl = impl_type_path(reflect_struct.meta()); - let full_reflect_impl = impl_full_reflect(reflect_struct.meta(), &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods( reflect_struct.meta(), || Some(quote!(#bevy_reflect_path::tuple_struct_partial_eq)), @@ -42,8 +38,7 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = - crate::impls::impl_function_traits(reflect_struct.meta(), &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let (impl_generics, ty_generics, where_clause) = reflect_struct .meta() diff --git a/crates/bevy_reflect/derive/src/impls/typed.rs b/crates/bevy_reflect/derive/src/impls/typed.rs index da8254d149..d4b0644976 100644 --- a/crates/bevy_reflect/derive/src/impls/typed.rs +++ b/crates/bevy_reflect/derive/src/impls/typed.rs @@ -138,10 +138,10 @@ pub(crate) fn impl_type_path(meta: &ReflectMeta) -> TokenStream { } pub(crate) fn impl_typed( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, type_info_generator: TokenStream, ) -> TokenStream { + let meta = where_clause_options.meta(); let type_path = meta.type_path(); let bevy_reflect_path = meta.bevy_reflect_path(); diff --git a/crates/bevy_reflect/derive/src/lib.rs b/crates/bevy_reflect/derive/src/lib.rs index 2d9dfca681..7ee7ad83e7 100644 --- a/crates/bevy_reflect/derive/src/lib.rs +++ b/crates/bevy_reflect/derive/src/lib.rs @@ -231,11 +231,11 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre /// // Generates a where clause like: /// // impl bevy_reflect::Reflect for Foo /// // where -/// // Self: Any + Send + Sync, -/// // Vec: FromReflect + TypePath, +/// // Foo: Any + Send + Sync, +/// // Vec: FromReflect + TypePath + MaybeTyped + RegisterForReflection, /// ``` /// -/// In this case, `Foo` is given the bounds `Vec: FromReflect + TypePath`, +/// In this case, `Foo` is given the bounds `Vec: FromReflect + ...`, /// which requires that `Foo` implements `FromReflect`, /// which requires that `Vec` implements `FromReflect`, /// and so on, resulting in the error. @@ -283,10 +283,10 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre /// // /// // impl bevy_reflect::Reflect for Foo /// // where -/// // Self: Any + Send + Sync, +/// // Foo: Any + Send + Sync, /// // T::Assoc: Default, /// // T: TypePath, -/// // T::Assoc: FromReflect + TypePath, +/// // T::Assoc: FromReflect + TypePath + MaybeTyped + RegisterForReflection, /// // T::Assoc: List, /// // {/* ... */} /// ``` diff --git a/crates/bevy_reflect/derive/src/registration.rs b/crates/bevy_reflect/derive/src/registration.rs index f60791215c..2d8174cfb6 100644 --- a/crates/bevy_reflect/derive/src/registration.rs +++ b/crates/bevy_reflect/derive/src/registration.rs @@ -1,19 +1,16 @@ //! Contains code related specifically to Bevy's type registration. -use crate::{ - derive_data::ReflectMeta, serialization::SerializationDataDef, - where_clause_options::WhereClauseOptions, -}; +use crate::{serialization::SerializationDataDef, where_clause_options::WhereClauseOptions}; use quote::quote; use syn::Type; /// Creates the `GetTypeRegistration` impl for the given type data. pub(crate) fn impl_get_type_registration<'a>( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, serialization_data: Option<&SerializationDataDef>, type_dependencies: Option>, ) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let type_path = meta.type_path(); let bevy_reflect_path = meta.bevy_reflect_path(); let registration_data = meta.attrs().idents(); @@ -46,7 +43,6 @@ pub(crate) fn impl_get_type_registration<'a>( }); quote! { - #[allow(unused_mut)] impl #impl_generics #bevy_reflect_path::GetTypeRegistration for #type_path #ty_generics #where_reflect_clause { fn get_type_registration() -> #bevy_reflect_path::TypeRegistration { let mut registration = #bevy_reflect_path::TypeRegistration::of::(); diff --git a/crates/bevy_reflect/derive/src/where_clause_options.rs b/crates/bevy_reflect/derive/src/where_clause_options.rs index 1551e008d0..d2d3b15a44 100644 --- a/crates/bevy_reflect/derive/src/where_clause_options.rs +++ b/crates/bevy_reflect/derive/src/where_clause_options.rs @@ -1,8 +1,8 @@ use crate::derive_data::ReflectMeta; use bevy_macro_utils::fq_std::{FQAny, FQSend, FQSync}; -use proc_macro2::TokenStream; +use proc_macro2::{TokenStream, TokenTree}; use quote::{quote, ToTokens}; -use syn::{punctuated::Punctuated, Token, Type, WhereClause}; +use syn::{punctuated::Punctuated, Ident, Token, Type, WhereClause}; /// Options defining how to extend the `where` clause for reflection. pub(crate) struct WhereClauseOptions<'a, 'b> { @@ -25,15 +25,28 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { } } - /// Extends the `where` clause for a type with additional bounds needed for the reflection impls. + pub fn meta(&self) -> &'a ReflectMeta<'b> { + self.meta + } + + /// Extends the `where` clause for a type with additional bounds needed for the reflection + /// impls. /// /// The default bounds added are as follows: - /// - `Self` has the bounds `Any + Send + Sync` - /// - Type parameters have the bound `TypePath` unless `#[reflect(type_path = false)]` is present - /// - Active fields have the bounds `TypePath` and either `PartialReflect` if `#[reflect(from_reflect = false)]` is present - /// or `FromReflect` otherwise (or no bounds at all if `#[reflect(no_field_bounds)]` is present) + /// - `Self` has: + /// - `Any + Send + Sync` bounds, if generic over types + /// - An `Any` bound, if generic over lifetimes but not types + /// - No bounds, if generic over neither types nor lifetimes + /// - Any given bounds in a `where` clause on the type + /// - Type parameters have the bound `TypePath` unless `#[reflect(type_path = false)]` is + /// present + /// - Active fields with non-generic types have the bounds `TypePath`, either `PartialReflect` + /// if `#[reflect(from_reflect = false)]` is present or `FromReflect` otherwise, + /// `MaybeTyped`, and `RegisterForReflection` (or no bounds at all if + /// `#[reflect(no_field_bounds)]` is present) /// - /// When the derive is used with `#[reflect(where)]`, the bounds specified in the attribute are added as well. + /// When the derive is used with `#[reflect(where)]`, the bounds specified in the attribute are + /// added as well. /// /// # Example /// @@ -51,57 +64,69 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { /// ```ignore (bevy_reflect is not accessible from this crate) /// where /// // `Self` bounds: - /// Self: Any + Send + Sync, + /// Foo: Any + Send + Sync, /// // Type parameter bounds: /// T: TypePath, /// U: TypePath, - /// // Field bounds - /// T: FromReflect + TypePath, + /// // Active non-generic field bounds + /// T: FromReflect + TypePath + MaybeTyped + RegisterForReflection, + /// /// ``` /// - /// If we had added `#[reflect(where T: MyTrait)]` to the type, it would instead generate: + /// If we add various things to the type: + /// + /// ```ignore (bevy_reflect is not accessible from this crate) + /// #[derive(Reflect)] + /// #[reflect(where T: MyTrait)] + /// #[reflect(no_field_bounds)] + /// struct Foo + /// where T: Clone + /// { + /// a: T, + /// #[reflect(ignore)] + /// b: U + /// } + /// ``` + /// + /// It will instead generate the following where clause: /// /// ```ignore (bevy_reflect is not accessible from this crate) /// where /// // `Self` bounds: - /// Self: Any + Send + Sync, - /// // Type parameter bounds: - /// T: TypePath, - /// U: TypePath, - /// // Field bounds - /// T: FromReflect + TypePath, - /// // Custom bounds - /// T: MyTrait, - /// ``` - /// - /// And if we also added `#[reflect(no_field_bounds)]` to the type, it would instead generate: - /// - /// ```ignore (bevy_reflect is not accessible from this crate) - /// where - /// // `Self` bounds: - /// Self: Any + Send + Sync, + /// Foo: Any + Send + Sync, + /// // Given bounds: + /// T: Clone, /// // Type parameter bounds: /// T: TypePath, /// U: TypePath, + /// // No active non-generic field bounds /// // Custom bounds /// T: MyTrait, /// ``` pub fn extend_where_clause(&self, where_clause: Option<&WhereClause>) -> TokenStream { - // We would normally just use `Self`, but that won't work for generating things like assertion functions - // and trait impls for a type's reference (e.g. `impl FromArg for &MyType`) - let this = self.meta.type_path().true_type(); + let mut generic_where_clause = quote! { where }; - let required_bounds = self.required_bounds(); + // Bounds on `Self`. We would normally just use `Self`, but that won't work for generating + // things like assertion functions and trait impls for a type's reference (e.g. `impl + // FromArg for &MyType`). + let generics = self.meta.type_path().generics(); + if generics.type_params().next().is_some() { + // Generic over types? We need `Any + Send + Sync`. + let this = self.meta.type_path().true_type(); + generic_where_clause.extend(quote! { #this: #FQAny + #FQSend + #FQSync, }); + } else if generics.lifetimes().next().is_some() { + // Generic only over lifetimes? We need `'static`. + let this = self.meta.type_path().true_type(); + generic_where_clause.extend(quote! { #this: 'static, }); + } - // Maintain existing where clause, if any. - let mut generic_where_clause = if let Some(where_clause) = where_clause { + // Maintain existing where clause bounds, if any. + if let Some(where_clause) = where_clause { let predicates = where_clause.predicates.iter(); - quote! {where #this: #required_bounds, #(#predicates,)*} - } else { - quote!(where #this: #required_bounds,) - }; + generic_where_clause.extend(quote! { #(#predicates,)* }); + } - // Add additional reflection trait bounds + // Add additional reflection trait bounds. let predicates = self.predicates(); generic_where_clause.extend(quote! { #predicates @@ -153,19 +178,57 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { let bevy_reflect_path = self.meta.bevy_reflect_path(); let reflect_bound = self.reflect_bound(); - // `TypePath` is always required for active fields since they are used to - // construct `NamedField` and `UnnamedField` instances for the `Typed` impl. - // Likewise, `GetTypeRegistration` is always required for active fields since - // they are used to register the type's dependencies. - Some(self.active_fields.iter().map(move |ty| { - quote!( - #ty : #reflect_bound - + #bevy_reflect_path::TypePath - // Needed for `Typed` impls - + #bevy_reflect_path::MaybeTyped - // Needed for `GetTypeRegistration` impls - + #bevy_reflect_path::__macro_exports::RegisterForReflection - ) + // Get the identifiers of all type parameters. + let type_param_idents = self + .meta + .type_path() + .generics() + .type_params() + .map(|type_param| type_param.ident.clone()) + .collect::>(); + + // Do any of the identifiers in `idents` appear in `token_stream`? + fn is_any_ident_in_token_stream(idents: &[Ident], token_stream: TokenStream) -> bool { + for token_tree in token_stream { + match token_tree { + TokenTree::Ident(ident) => { + if idents.contains(&ident) { + return true; + } + } + TokenTree::Group(group) => { + if is_any_ident_in_token_stream(idents, group.stream()) { + return true; + } + } + TokenTree::Punct(_) | TokenTree::Literal(_) => {} + } + } + false + } + + Some(self.active_fields.iter().filter_map(move |ty| { + // Field type bounds are only required if `ty` is generic. How to determine that? + // Search `ty`s token stream for identifiers that match the identifiers from the + // function's type params. E.g. if `T` and `U` are the type param identifiers and + // `ty` is `Vec<[T; 4]>` then the `T` identifiers match. This is a bit hacky, but + // it works. + let is_generic = + is_any_ident_in_token_stream(&type_param_idents, ty.to_token_stream()); + + is_generic.then(|| { + quote!( + #ty: #reflect_bound + // Needed to construct `NamedField` and `UnnamedField` instances for + // the `Typed` impl. + + #bevy_reflect_path::TypePath + // Needed for `Typed` impls + + #bevy_reflect_path::MaybeTyped + // Needed for registering type dependencies in the + // `GetTypeRegistration` impl. + + #bevy_reflect_path::__macro_exports::RegisterForReflection + ) + }) })) } } @@ -190,9 +253,4 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { None } } - - /// The minimum required bounds for a type to be reflected. - fn required_bounds(&self) -> TokenStream { - quote!(#FQAny + #FQSend + #FQSync) - } } diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 8be0110a3e..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 { @@ -186,8 +187,7 @@ impl DynamicArray { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Array(_)), - "expected TypeInfo::Array but received: {:?}", - represented_type + "expected TypeInfo::Array but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/attributes.rs b/crates/bevy_reflect/src/attributes.rs index a6edefab25..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 } @@ -213,7 +218,7 @@ mod tests { fn should_debug_custom_attributes() { let attributes = CustomAttributes::default().with_attribute("My awesome custom attribute!"); - let debug = format!("{:?}", attributes); + let debug = format!("{attributes:?}"); assert_eq!(r#"{"My awesome custom attribute!"}"#, debug); @@ -224,7 +229,7 @@ mod tests { let attributes = CustomAttributes::default().with_attribute(Foo { value: 42 }); - let debug = format!("{:?}", attributes); + let debug = format!("{attributes:?}"); assert_eq!( r#"{bevy_reflect::attributes::tests::Foo { value: 42 }}"#, diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index 42c20e1956..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), } @@ -114,8 +117,7 @@ impl DynamicEnum { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Enum(_)), - "expected TypeInfo::Enum but received: {:?}", - represented_type + "expected TypeInfo::Enum but received: {represented_type:?}", ); } 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 e783a33775..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>, }, } @@ -55,7 +66,7 @@ fn full_path( container_type_path: &str, ) -> alloc::string::String { match variant { - Some(variant) => format!("{}::{}::{}", container_type_path, variant, field), - None => format!("{}::{}", container_type_path, field), + Some(variant) => format!("{container_type_path}::{variant}::{field}"), + None => format!("{container_type_path}::{field}"), } } 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 7a5da57525..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>); @@ -550,7 +551,7 @@ mod tests { fn should_clone_dynamic_function() { let hello = String::from("Hello"); - let greet = |name: &String| -> String { format!("{}, {}!", hello, name) }; + let greet = |name: &String| -> String { format!("{hello}, {name}!") }; let greet = greet.into_function().with_name("greet"); let clone = greet.clone(); @@ -771,18 +772,18 @@ mod tests { #[test] fn should_debug_dynamic_function() { fn greet(name: &String) -> String { - format!("Hello, {}!", name) + format!("Hello, {name}!") } let function = greet.into_function(); - let debug = format!("{:?}", function); + let debug = format!("{function:?}"); assert_eq!(debug, "DynamicFunction(fn bevy_reflect::func::dynamic_function::tests::should_debug_dynamic_function::greet(_: &alloc::string::String) -> alloc::string::String)"); } #[test] fn should_debug_anonymous_dynamic_function() { let function = (|a: i32, b: i32| a + b).into_function(); - let debug = format!("{:?}", function); + let debug = format!("{function:?}"); assert_eq!(debug, "DynamicFunction(fn _(_: i32, _: i32) -> i32)"); } @@ -792,11 +793,11 @@ mod tests { a + b } - let func = add:: + let function = add:: .into_function() .with_overload(add::) .with_name("add"); - let debug = format!("{:?}", func); + let debug = format!("{function:?}"); assert_eq!( debug, "DynamicFunction(fn add{(_: i32, _: i32) -> i32, (_: f32, _: f32) -> f32})" 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 53737fd891..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>, @@ -434,7 +440,7 @@ impl<'a> Debug for PrettyPrintFunctionInfo<'a> { } match (self.include_name, self.info.name()) { - (true, Some(name)) => write!(f, "{}", name)?, + (true, Some(name)) => write!(f, "{name}")?, (true, None) => write!(f, "_")?, _ => {} } @@ -509,7 +515,7 @@ impl<'a> Debug for PrettyPrintSignatureInfo<'a> { } match (self.include_name, self.info.name()) { - (true, Some(name)) => write!(f, "{}", name)?, + (true, Some(name)) => write!(f, "{name}")?, (true, None) => write!(f, "_")?, _ => {} } 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 58a8344ecf..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>, } @@ -520,7 +521,7 @@ mod tests { let mut registry = FunctionRegistry::default(); registry.register_with_name("foo", foo).unwrap(); - let debug = format!("{:?}", registry); + let debug = format!("{registry:?}"); assert_eq!(debug, "{DynamicFunction(fn foo() -> i32)}"); } } diff --git a/crates/bevy_reflect/src/func/return_type.rs b/crates/bevy_reflect/src/func/return_type.rs index bab3c04b25..9abe0ef32c 100644 --- a/crates/bevy_reflect/src/func/return_type.rs +++ b/crates/bevy_reflect/src/func/return_type.rs @@ -129,7 +129,7 @@ macro_rules! impl_into_return { )? { fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { - $crate::func::Return::Owned(Box::new(self)) + $crate::func::Return::Owned(bevy_platform::prelude::Box::new(self)) } } diff --git a/crates/bevy_reflect/src/func/signature.rs b/crates/bevy_reflect/src/func/signature.rs index 7813d7d4f9..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()) @@ -229,7 +230,7 @@ mod tests { ); assert_eq!( - format!("{:?}", signature), + format!("{signature:?}"), "(&mut alloc::string::String, i32) -> ()" ); } diff --git a/crates/bevy_reflect/src/impls/alloc/borrow.rs b/crates/bevy_reflect/src/impls/alloc/borrow.rs new file mode 100644 index 0000000000..9021343c67 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/borrow.rs @@ -0,0 +1,308 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + list::{List, ListInfo, ListIter}, + prelude::*, + reflect::{impl_full_reflect, ApplyError}, + type_info::{MaybeTyped, OpaqueInfo, TypeInfo, Typed}, + type_registry::{ + FromType, GetTypeRegistration, ReflectDeserialize, ReflectFromPtr, ReflectSerialize, + TypeRegistration, TypeRegistry, + }, + utility::{reflect_hasher, GenericTypeInfoCell, NonGenericTypeInfoCell}, +}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use bevy_platform::prelude::*; +use bevy_reflect_derive::impl_type_path; +use core::any::Any; +use core::fmt; +use core::hash::{Hash, Hasher}; + +impl_type_path!(::alloc::borrow::Cow<'a: 'static, T: ToOwned + ?Sized>); + +impl PartialReflect for Cow<'static, str> { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(self.clone())) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static + to_type: Self::type_path().into(), + }); + } + Ok(()) + } +} + +impl_full_reflect!(for Cow<'static, str>); + +impl Typed for Cow<'static, str> { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for Cow<'static, str> { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::>(); + registration.insert::(FromType::>::from_type()); + registration.insert::(FromType::>::from_type()); + registration.insert::(FromType::>::from_type()); + registration.insert::(FromType::>::from_type()); + registration + } +} + +impl FromReflect for Cow<'static, str> { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::>()?.clone()) + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, str>); + +impl List + for Cow<'static, [T]> +{ + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { + self.as_ref().get(index).map(|x| x as &dyn PartialReflect) + } + + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + self.to_mut() + .get_mut(index) + .map(|x| x as &mut dyn PartialReflect) + } + + fn insert(&mut self, index: usize, element: Box) { + let value = T::take_from_reflect(element).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ); + }); + self.to_mut().insert(index, value); + } + + fn remove(&mut self, index: usize) -> Box { + Box::new(self.to_mut().remove(index)) + } + + fn push(&mut self, value: Box) { + let value = T::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to push invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.to_mut().push(value); + } + + fn pop(&mut self) -> Option> { + self.to_mut() + .pop() + .map(|value| Box::new(value) as Box) + } + + fn len(&self) -> usize { + self.as_ref().len() + } + + fn iter(&self) -> ListIter { + ListIter::new(self) + } + + fn drain(&mut self) -> Vec> { + self.to_mut() + .drain(..) + .map(|value| Box::new(value) as Box) + .collect() + } +} + +impl PartialReflect + for Cow<'static, [T]> +{ + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::List + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::List(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::List(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::List(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(self.clone())) + } + + fn reflect_hash(&self) -> Option { + crate::list_hash(self) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + crate::list_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::list_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } +} + +impl_full_reflect!( + for Cow<'static, [T]> + where + T: FromReflect + Clone + MaybeTyped + TypePath + GetTypeRegistration, +); + +impl Typed + for Cow<'static, [T]> +{ + fn type_info() -> &'static TypeInfo { + static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| TypeInfo::List(ListInfo::new::())) + } +} + +impl GetTypeRegistration + for Cow<'static, [T]> +{ + fn get_type_registration() -> TypeRegistration { + TypeRegistration::of::>() + } + + fn register_type_dependencies(registry: &mut TypeRegistry) { + registry.register::(); + } +} + +impl FromReflect + for Cow<'static, [T]> +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + let ref_list = reflect.reflect_ref().as_list().ok()?; + + let mut temp_vec = Vec::with_capacity(ref_list.len()); + + for field in ref_list.iter() { + temp_vec.push(T::from_reflect(field)?); + } + + Some(temp_vec.into()) + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, [T]>; ); diff --git a/crates/bevy_reflect/src/impls/alloc/collections/binary_heap.rs b/crates/bevy_reflect/src/impls/alloc/collections/binary_heap.rs new file mode 100644 index 0000000000..38f16f5851 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/binary_heap.rs @@ -0,0 +1,28 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::alloc::collections::BinaryHeap(Clone)); + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + use bevy_reflect::Reflect; + + #[test] + fn should_partial_eq_btree_map() { + let mut a = BTreeMap::new(); + a.insert(0usize, 1.23_f64); + let b = a.clone(); + let mut c = BTreeMap::new(); + c.insert(0usize, 3.21_f64); + + let a: &dyn Reflect = &a; + let b: &dyn Reflect = &b; + let c: &dyn Reflect = &c; + assert!(a + .reflect_partial_eq(b.as_partial_reflect()) + .unwrap_or_default()); + assert!(!a + .reflect_partial_eq(c.as_partial_reflect()) + .unwrap_or_default()); + } +} diff --git a/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs b/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs new file mode 100644 index 0000000000..d5559a2985 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs @@ -0,0 +1,232 @@ +use crate::{ + error::ReflectCloneError, + generics::{Generics, TypeParamInfo}, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + map::{map_apply, map_partial_eq, map_try_apply, Map, MapInfo}, + prelude::*, + reflect::{impl_full_reflect, ApplyError}, + type_info::{MaybeTyped, TypeInfo, Typed}, + type_registry::{FromType, GetTypeRegistration, ReflectFromPtr, TypeRegistration}, + utility::GenericTypeInfoCell, +}; +use alloc::vec::Vec; +use bevy_platform::prelude::*; +use bevy_reflect_derive::impl_type_path; + +impl Map for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { + key.try_downcast_ref::() + .and_then(|key| Self::get(self, key)) + .map(|value| value as &dyn PartialReflect) + } + + fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { + key.try_downcast_ref::() + .and_then(move |key| Self::get_mut(self, key)) + .map(|value| value as &mut dyn PartialReflect) + } + + fn len(&self) -> usize { + Self::len(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)> { + // BTreeMap doesn't have a `drain` function. See + // https://github.com/rust-lang/rust/issues/81074. So we have to fake one by popping + // elements off one at a time. + let mut result = Vec::with_capacity(self.len()); + while let Some((k, v)) = self.pop_first() { + result.push(( + Box::new(k) as Box, + Box::new(v) as Box, + )); + } + 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, + value: Box, + ) -> Option> { + let key = K::take_from_reflect(key).unwrap_or_else(|key| { + panic!( + "Attempted to insert invalid key of type {}.", + key.reflect_type_path() + ) + }); + let value = V::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.insert(key, value) + .map(|old_value| Box::new(old_value) as Box) + } + + fn remove(&mut self, key: &dyn PartialReflect) -> Option> { + let mut from_reflect = None; + key.try_downcast_ref::() + .or_else(|| { + from_reflect = K::from_reflect(key); + from_reflect.as_ref() + }) + .and_then(|key| self.remove(key)) + .map(|value| Box::new(value) as Box) + } +} + +impl PartialReflect for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + #[inline] + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Map + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Map(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Map(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Map(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + let mut map = Self::new(); + for (key, value) in self.iter() { + let key = key.reflect_clone_and_take()?; + let value = value.reflect_clone_and_take()?; + map.insert(key, value); + } + + Ok(Box::new(map)) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + map_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn PartialReflect) { + map_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } +} + +impl_full_reflect!( + for ::alloc::collections::BTreeMap + where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +); + +impl Typed for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn type_info() -> &'static TypeInfo { + static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + TypeInfo::Map( + MapInfo::new::().with_generics(Generics::from_iter([ + TypeParamInfo::new::("K"), + TypeParamInfo::new::("V"), + ])), + ) + }) + } +} + +impl GetTypeRegistration for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + let ref_map = reflect.reflect_ref().as_map().ok()?; + + let mut new_map = Self::new(); + + for (key, value) in ref_map.iter() { + let new_key = K::from_reflect(key)?; + let new_value = V::from_reflect(value)?; + new_map.insert(new_key, new_value); + } + + Some(new_map) + } +} + +impl_type_path!(::alloc::collections::BTreeMap); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::alloc::collections::BTreeMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + > +); diff --git a/crates/bevy_reflect/src/impls/alloc/collections/btree/mod.rs b/crates/bevy_reflect/src/impls/alloc/collections/btree/mod.rs new file mode 100644 index 0000000000..095ca5dd2e --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/btree/mod.rs @@ -0,0 +1,2 @@ +mod map; +mod set; diff --git a/crates/bevy_reflect/src/impls/alloc/collections/btree/set.rs b/crates/bevy_reflect/src/impls/alloc/collections/btree/set.rs new file mode 100644 index 0000000000..41d711f205 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/btree/set.rs @@ -0,0 +1,3 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::alloc::collections::BTreeSet(Clone)); diff --git a/crates/bevy_reflect/src/impls/alloc/collections/mod.rs b/crates/bevy_reflect/src/impls/alloc/collections/mod.rs new file mode 100644 index 0000000000..dcf75ce03c --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/mod.rs @@ -0,0 +1,3 @@ +mod binary_heap; +mod btree; +mod vec_deque; diff --git a/crates/bevy_reflect/src/impls/alloc/collections/vec_deque.rs b/crates/bevy_reflect/src/impls/alloc/collections/vec_deque.rs new file mode 100644 index 0000000000..5144bc3c2b --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/vec_deque.rs @@ -0,0 +1,20 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_veclike; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; + +impl_reflect_for_veclike!( + ::alloc::collections::VecDeque, + ::alloc::collections::VecDeque::insert, + ::alloc::collections::VecDeque::remove, + ::alloc::collections::VecDeque::push_back, + ::alloc::collections::VecDeque::pop_back, + ::alloc::collections::VecDeque:: +); +impl_type_path!(::alloc::collections::VecDeque); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::alloc::collections::VecDeque; ); diff --git a/crates/bevy_reflect/src/impls/alloc/mod.rs b/crates/bevy_reflect/src/impls/alloc/mod.rs new file mode 100644 index 0000000000..a49fa5599f --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/mod.rs @@ -0,0 +1,4 @@ +mod borrow; +mod collections; +mod string; +mod vec; diff --git a/crates/bevy_reflect/src/impls/alloc/string.rs b/crates/bevy_reflect/src/impls/alloc/string.rs new file mode 100644 index 0000000000..627082ee75 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/string.rs @@ -0,0 +1,30 @@ +use crate::{ + std_traits::ReflectDefault, + type_registry::{ReflectDeserialize, ReflectSerialize}, +}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::alloc::string::String( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); + +#[cfg(test)] +mod tests { + use alloc::string::String; + use bevy_reflect::PartialReflect; + + #[test] + fn should_partial_eq_string() { + let a: &dyn PartialReflect = &String::from("Hello"); + let b: &dyn PartialReflect = &String::from("Hello"); + let c: &dyn PartialReflect = &String::from("World"); + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } +} diff --git a/crates/bevy_reflect/src/impls/alloc/vec.rs b/crates/bevy_reflect/src/impls/alloc/vec.rs new file mode 100644 index 0000000000..4e4c819d6a --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/vec.rs @@ -0,0 +1,35 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_veclike; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; + +impl_reflect_for_veclike!( + ::alloc::vec::Vec, + ::alloc::vec::Vec::insert, + ::alloc::vec::Vec::remove, + ::alloc::vec::Vec::push, + ::alloc::vec::Vec::pop, + [T] +); +impl_type_path!(::alloc::vec::Vec); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::alloc::vec::Vec; ); + +#[cfg(test)] +mod tests { + use alloc::vec; + use bevy_reflect::PartialReflect; + + #[test] + fn should_partial_eq_vec() { + let a: &dyn PartialReflect = &vec![1, 2, 3]; + let b: &dyn PartialReflect = &vec![1, 2, 3]; + let c: &dyn PartialReflect = &vec![3, 2, 1]; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } +} diff --git a/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_map.rs b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_map.rs new file mode 100644 index 0000000000..48f0ddcd89 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_map.rs @@ -0,0 +1,47 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashmap; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashmap!(bevy_platform::collections::HashMap); +impl_type_path!(::bevy_platform::collections::HashMap); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +#[cfg(test)] +mod tests { + use crate::{PartialReflect, Reflect}; + use static_assertions::assert_impl_all; + + #[test] + fn should_partial_eq_hash_map() { + let mut a = >::default(); + a.insert(0usize, 1.23_f64); + let b = a.clone(); + let mut c = >::default(); + c.insert(0usize, 3.21_f64); + + let a: &dyn PartialReflect = &a; + let b: &dyn PartialReflect = &b; + let c: &dyn PartialReflect = &c; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn should_reflect_hashmaps() { + assert_impl_all!(bevy_platform::collections::HashMap: Reflect); + } +} diff --git a/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_set.rs b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_set.rs new file mode 100644 index 0000000000..a9c3f7a959 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_set.rs @@ -0,0 +1,17 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashset; +#[cfg(feature = "functions")] +use crate::{from_reflect::FromReflect, type_path::TypePath, type_registry::GetTypeRegistration}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashset!(::bevy_platform::collections::HashSet); +impl_type_path!(::bevy_platform::collections::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); diff --git a/crates/bevy_reflect/src/impls/bevy_platform/collections/mod.rs b/crates/bevy_reflect/src/impls/bevy_platform/collections/mod.rs new file mode 100644 index 0000000000..2bde6a0653 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/collections/mod.rs @@ -0,0 +1,2 @@ +mod hash_map; +mod hash_set; diff --git a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs new file mode 100644 index 0000000000..c2a38dd5f3 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs @@ -0,0 +1,5 @@ +use bevy_reflect_derive::impl_type_path; + +impl_type_path!(::bevy_platform::hash::NoOpHash); +impl_type_path!(::bevy_platform::hash::FixedHasher); +impl_type_path!(::bevy_platform::hash::PassHash); diff --git a/crates/bevy_reflect/src/impls/bevy_platform/mod.rs b/crates/bevy_reflect/src/impls/bevy_platform/mod.rs new file mode 100644 index 0000000000..aa7f15eb5e --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/mod.rs @@ -0,0 +1,4 @@ +mod collections; +mod hash; +mod sync; +mod time; diff --git a/crates/bevy_reflect/src/impls/bevy_platform/sync.rs b/crates/bevy_reflect/src/impls/bevy_platform/sync.rs new file mode 100644 index 0000000000..ba3234177f --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/sync.rs @@ -0,0 +1,3 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::bevy_platform::sync::Arc(Clone)); diff --git a/crates/bevy_reflect/src/impls/bevy_platform/time.rs b/crates/bevy_reflect/src/impls/bevy_platform/time.rs new file mode 100644 index 0000000000..7120359dc4 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/time.rs @@ -0,0 +1,18 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::bevy_platform::time::Instant( + Clone, Debug, Hash, PartialEq +)); + +#[cfg(test)] +mod tests { + use crate::FromReflect; + use bevy_platform::time::Instant; + + #[test] + fn instant_should_from_reflect() { + let expected = Instant::now(); + let output = Instant::from_reflect(&expected).unwrap(); + assert_eq!(expected, output); + } +} diff --git a/crates/bevy_reflect/src/impls/core/any.rs b/crates/bevy_reflect/src/impls/core/any.rs new file mode 100644 index 0000000000..062768c341 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/any.rs @@ -0,0 +1,15 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::any::TypeId(Clone, Debug, Hash, PartialEq,)); + +#[cfg(test)] +mod tests { + use bevy_reflect::FromReflect; + + #[test] + fn type_id_should_from_reflect() { + let type_id = core::any::TypeId::of::(); + let output = ::from_reflect(&type_id).unwrap(); + assert_eq!(type_id, output); + } +} diff --git a/crates/bevy_reflect/src/impls/core/hash.rs b/crates/bevy_reflect/src/impls/core/hash.rs new file mode 100644 index 0000000000..6301400091 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/hash.rs @@ -0,0 +1,3 @@ +use bevy_reflect_derive::impl_type_path; + +impl_type_path!(::core::hash::BuildHasherDefault); diff --git a/crates/bevy_reflect/src/impls/core/mod.rs b/crates/bevy_reflect/src/impls/core/mod.rs new file mode 100644 index 0000000000..cf20471410 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/mod.rs @@ -0,0 +1,11 @@ +mod any; +mod hash; +mod net; +mod num; +mod ops; +mod option; +mod panic; +mod primitives; +mod result; +mod sync; +mod time; diff --git a/crates/bevy_reflect/src/impls/core/net.rs b/crates/bevy_reflect/src/impls/core/net.rs new file mode 100644 index 0000000000..965308680b --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/net.rs @@ -0,0 +1,11 @@ +use crate::type_registry::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::net::SocketAddr( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); diff --git a/crates/bevy_reflect/src/impls/core/num.rs b/crates/bevy_reflect/src/impls/core/num.rs new file mode 100644 index 0000000000..e64744f69e --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/num.rs @@ -0,0 +1,115 @@ +use crate::type_registry::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::num::NonZeroI128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroIsize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroUsize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::Wrapping(Clone)); +impl_reflect_opaque!(::core::num::Saturating(Clone)); + +#[cfg(test)] +mod tests { + use bevy_reflect::{FromReflect, PartialReflect}; + + #[test] + fn nonzero_usize_impl_reflect_from_reflect() { + let a: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); + let b: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + let forty_two: core::num::NonZero = FromReflect::from_reflect(a).unwrap(); + assert_eq!(forty_two, core::num::NonZero::::new(42).unwrap()); + } +} diff --git a/crates/bevy_reflect/src/impls/core/ops.rs b/crates/bevy_reflect/src/impls/core/ops.rs new file mode 100644 index 0000000000..fdbe87a802 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/ops.rs @@ -0,0 +1,9 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::ops::Range(Clone)); +impl_reflect_opaque!(::core::ops::RangeInclusive(Clone)); +impl_reflect_opaque!(::core::ops::RangeFrom(Clone)); +impl_reflect_opaque!(::core::ops::RangeTo(Clone)); +impl_reflect_opaque!(::core::ops::RangeToInclusive(Clone)); +impl_reflect_opaque!(::core::ops::RangeFull(Clone)); +impl_reflect_opaque!(::core::ops::Bound(Clone)); diff --git a/crates/bevy_reflect/src/impls/core/option.rs b/crates/bevy_reflect/src/impls/core/option.rs new file mode 100644 index 0000000000..fe8318806d --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/option.rs @@ -0,0 +1,138 @@ +#![expect( + unused_qualifications, + reason = "the macro uses `MyEnum::Variant` which is generally unnecessary for `Option`" +)] + +use bevy_reflect_derive::impl_reflect; + +impl_reflect! { + #[type_path = "core::option"] + enum Option { + None, + Some(T), + } +} + +#[cfg(test)] +mod tests { + use crate::{Enum, FromReflect, PartialReflect, TypeInfo, Typed, VariantInfo, VariantType}; + use bevy_reflect_derive::Reflect; + use static_assertions::assert_impl_all; + + #[test] + fn should_partial_eq_option() { + let a: &dyn PartialReflect = &Some(123); + let b: &dyn PartialReflect = &Some(123); + assert_eq!(Some(true), a.reflect_partial_eq(b)); + } + + #[test] + fn option_should_impl_enum() { + assert_impl_all!(Option<()>: Enum); + + let mut value = Some(123usize); + + assert!(value + .reflect_partial_eq(&Some(123usize)) + .unwrap_or_default()); + assert!(!value + .reflect_partial_eq(&Some(321usize)) + .unwrap_or_default()); + + assert_eq!("Some", value.variant_name()); + assert_eq!("core::option::Option::Some", value.variant_path()); + + if value.is_variant(VariantType::Tuple) { + if let Some(field) = value + .field_at_mut(0) + .and_then(|field| field.try_downcast_mut::()) + { + *field = 321; + } + } else { + panic!("expected `VariantType::Tuple`"); + } + + assert_eq!(Some(321), value); + } + + #[test] + fn option_should_from_reflect() { + #[derive(Reflect, PartialEq, Debug)] + struct Foo(usize); + + let expected = Some(Foo(123)); + let output = as FromReflect>::from_reflect(&expected).unwrap(); + + assert_eq!(expected, output); + } + + #[test] + fn option_should_apply() { + #[derive(Reflect, PartialEq, Debug)] + struct Foo(usize); + + // === None on None === // + let patch = None::; + let mut value = None::; + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "None apply onto None"); + + // === Some on None === // + let patch = Some(Foo(123)); + let mut value = None::; + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "Some apply onto None"); + + // === None on Some === // + let patch = None::; + let mut value = Some(Foo(321)); + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "None apply onto Some"); + + // === Some on Some === // + let patch = Some(Foo(123)); + let mut value = Some(Foo(321)); + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "Some apply onto Some"); + } + + #[test] + fn option_should_impl_typed() { + assert_impl_all!(Option<()>: Typed); + + type MyOption = Option; + let info = MyOption::type_info(); + if let TypeInfo::Enum(info) = info { + assert_eq!( + "None", + info.variant_at(0).unwrap().name(), + "Expected `None` to be variant at index `0`" + ); + assert_eq!( + "Some", + info.variant_at(1).unwrap().name(), + "Expected `Some` to be variant at index `1`" + ); + assert_eq!("Some", info.variant("Some").unwrap().name()); + if let VariantInfo::Tuple(variant) = info.variant("Some").unwrap() { + assert!( + variant.field_at(0).unwrap().is::(), + "Expected `Some` variant to contain `i32`" + ); + assert!( + variant.field_at(1).is_none(), + "Expected `Some` variant to only contain 1 field" + ); + } else { + panic!("Expected `VariantInfo::Tuple`"); + } + } else { + panic!("Expected `TypeInfo::Enum`"); + } + } +} diff --git a/crates/bevy_reflect/src/impls/core/panic.rs b/crates/bevy_reflect/src/impls/core/panic.rs new file mode 100644 index 0000000000..75bf365422 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/panic.rs @@ -0,0 +1,158 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::ApplyError, + type_info::{OpaqueInfo, TypeInfo, Typed}, + type_path::DynamicTypePath, + type_registry::{FromType, GetTypeRegistration, ReflectFromPtr, TypeRegistration}, + utility::{reflect_hasher, NonGenericTypeInfoCell}, +}; +use bevy_platform::prelude::*; +use core::any::Any; +use core::hash::{Hash, Hasher}; +use core::panic::Location; + +impl TypePath for &'static Location<'static> { + fn type_path() -> &'static str { + "core::panic::Location" + } + + fn short_type_path() -> &'static str { + "Location" + } +} + +impl PartialReflect for &'static Location<'static> { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(*self)) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for &'static Location<'static> { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for &'static Location<'static> { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for &'static Location<'static> { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for &'static Location<'static> { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static Location<'static>); diff --git a/crates/bevy_reflect/src/impls/core/primitives.rs b/crates/bevy_reflect/src/impls/core/primitives.rs new file mode 100644 index 0000000000..3600f2ece5 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/primitives.rs @@ -0,0 +1,565 @@ +use crate::{ + array::{Array, ArrayInfo, ArrayIter}, + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::ApplyError, + type_info::{MaybeTyped, OpaqueInfo, TypeInfo, Typed}, + type_registry::{ + FromType, GetTypeRegistration, ReflectDeserialize, ReflectFromPtr, ReflectSerialize, + TypeRegistration, TypeRegistry, + }, + utility::{reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell}, +}; +use bevy_platform::prelude::*; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; +use core::any::Any; +use core::fmt; +use core::hash::{Hash, Hasher}; + +impl_reflect_opaque!(bool( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(char( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(usize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(isize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(f32( + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(f64( + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_type_path!(str); + +impl PartialReflect for &'static str { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(*self)) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self, f) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: Self::type_path().into(), + }); + } + Ok(()) + } +} + +impl Reflect for &'static str { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for &'static str { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for &'static str { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for &'static str { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static str); + +impl Array for [T; N] { + #[inline] + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { + <[T]>::get(self, index).map(|value| value as &dyn PartialReflect) + } + + #[inline] + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + <[T]>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) + } + + #[inline] + fn len(&self) -> usize { + N + } + + #[inline] + fn iter(&self) -> ArrayIter { + ArrayIter::new(self) + } + + #[inline] + fn drain(self: Box) -> Vec> { + self.into_iter() + .map(|value| Box::new(value) as Box) + .collect() + } +} + +impl PartialReflect + for [T; N] +{ + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + #[inline] + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Array + } + + #[inline] + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Array(self) + } + + #[inline] + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Array(self) + } + + #[inline] + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Array(self) + } + + #[inline] + fn reflect_hash(&self) -> Option { + crate::array_hash(self) + } + + #[inline] + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + crate::array_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::array_apply(self, value); + } + + #[inline] + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::array_try_apply(self, value) + } +} + +impl Reflect for [T; N] { + #[inline] + fn into_any(self: Box) -> Box { + self + } + + #[inline] + fn as_any(&self) -> &dyn Any { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + #[inline] + fn into_reflect(self: Box) -> Box { + self + } + + #[inline] + fn as_reflect(&self) -> &dyn Reflect { + self + } + + #[inline] + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + #[inline] + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl FromReflect + for [T; N] +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + let ref_array = reflect.reflect_ref().as_array().ok()?; + + let mut temp_vec = Vec::with_capacity(ref_array.len()); + + for field in ref_array.iter() { + temp_vec.push(T::from_reflect(field)?); + } + + temp_vec.try_into().ok() + } +} + +impl Typed for [T; N] { + fn type_info() -> &'static TypeInfo { + static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| TypeInfo::Array(ArrayInfo::new::(N))) + } +} + +impl TypePath for [T; N] { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::short_type_path())) + } +} + +impl GetTypeRegistration + for [T; N] +{ + fn get_type_registration() -> TypeRegistration { + TypeRegistration::of::<[T; N]>() + } + + fn register_type_dependencies(registry: &mut TypeRegistry) { + registry.register::(); + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!([T; N]; [const N: usize]); + +impl TypePath for [T] +where + [T]: ToOwned, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{}]", ::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{}]", ::short_type_path())) + } +} + +impl TypePath for &'static T { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&{}", T::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&{}", T::short_type_path())) + } +} + +impl TypePath for &'static mut T { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&mut {}", T::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&mut {}", T::short_type_path())) + } +} + +#[cfg(test)] +mod tests { + use bevy_reflect::{FromReflect, PartialReflect}; + use core::f32::consts::{PI, TAU}; + + #[test] + fn should_partial_eq_char() { + let a: &dyn PartialReflect = &'x'; + let b: &dyn PartialReflect = &'x'; + let c: &dyn PartialReflect = &'o'; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn should_partial_eq_i32() { + let a: &dyn PartialReflect = &123_i32; + let b: &dyn PartialReflect = &123_i32; + let c: &dyn PartialReflect = &321_i32; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn should_partial_eq_f32() { + let a: &dyn PartialReflect = &PI; + let b: &dyn PartialReflect = &PI; + let c: &dyn PartialReflect = &TAU; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn static_str_should_from_reflect() { + let expected = "Hello, World!"; + let output = <&'static str as FromReflect>::from_reflect(&expected).unwrap(); + assert_eq!(expected, output); + } +} diff --git a/crates/bevy_reflect/src/impls/core/result.rs b/crates/bevy_reflect/src/impls/core/result.rs new file mode 100644 index 0000000000..d601e0bd6f --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/result.rs @@ -0,0 +1,14 @@ +#![expect( + unused_qualifications, + reason = "the macro uses `MyEnum::Variant` which is generally unnecessary for `Result`" +)] + +use bevy_reflect_derive::impl_reflect; + +impl_reflect! { + #[type_path = "core::result"] + enum Result { + Ok(T), + Err(E), + } +} diff --git a/crates/bevy_reflect/src/impls/core/sync.rs b/crates/bevy_reflect/src/impls/core/sync.rs new file mode 100644 index 0000000000..06b930e8a9 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/sync.rs @@ -0,0 +1,179 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::{impl_full_reflect, ApplyError}, + type_info::{OpaqueInfo, TypeInfo, Typed}, + type_path::DynamicTypePath, + type_registry::{FromType, GetTypeRegistration, ReflectFromPtr, TypeRegistration}, + utility::NonGenericTypeInfoCell, +}; +use bevy_platform::prelude::*; +use bevy_reflect_derive::impl_type_path; +use core::fmt; + +macro_rules! impl_reflect_for_atomic { + ($ty:ty, $ordering:expr) => { + impl_type_path!($ty); + + const _: () = { + #[cfg(feature = "functions")] + crate::func::macros::impl_function_traits!($ty); + + impl GetTypeRegistration for $ty { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + + // Serde only supports atomic types when the "std" feature is enabled + #[cfg(feature = "std")] + { + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + } + + registration + } + } + + impl Typed for $ty { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| { + let info = OpaqueInfo::new::(); + TypeInfo::Opaque(info) + }) + } + } + + impl PartialReflect for $ty { + #[inline] + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + #[inline] + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + #[inline] + fn try_into_reflect( + self: Box, + ) -> Result, Box> { + Ok(self) + } + #[inline] + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + #[inline] + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + #[inline] + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(<$ty>::new(self.load($ordering)))) + } + + #[inline] + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + *self = <$ty>::new(value.load($ordering)); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: Into::into(DynamicTypePath::reflect_type_path(value)), + to_type: Into::into(::type_path()), + }); + } + Ok(()) + } + #[inline] + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + #[inline] + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + #[inline] + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + #[inline] + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } + } + + impl FromReflect for $ty { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(<$ty>::new( + reflect.try_downcast_ref::<$ty>()?.load($ordering), + )) + } + } + }; + + impl_full_reflect!(for $ty); + }; +} + +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicIsize, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicUsize, + ::core::sync::atomic::Ordering::SeqCst +); +#[cfg(target_has_atomic = "64")] +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI64, + ::core::sync::atomic::Ordering::SeqCst +); +#[cfg(target_has_atomic = "64")] +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU64, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI32, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU32, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI16, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU16, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI8, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU8, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicBool, + ::core::sync::atomic::Ordering::SeqCst +); diff --git a/crates/bevy_reflect/src/impls/core/time.rs b/crates/bevy_reflect/src/impls/core/time.rs new file mode 100644 index 0000000000..42e581b864 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/time.rs @@ -0,0 +1,32 @@ +use crate::{ + std_traits::ReflectDefault, + type_registry::{ReflectDeserialize, ReflectSerialize}, +}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::time::Duration( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); + +#[cfg(test)] +mod tests { + use bevy_reflect::{ReflectSerialize, TypeRegistry}; + use core::time::Duration; + + #[test] + fn can_serialize_duration() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::(); + + let reflect_serialize = type_registry + .get_type_data::(core::any::TypeId::of::()) + .unwrap(); + let _serializable = reflect_serialize.get_serializable(&Duration::ZERO); + } +} diff --git a/crates/bevy_reflect/src/impls/hashbrown.rs b/crates/bevy_reflect/src/impls/hashbrown.rs new file mode 100644 index 0000000000..4ea914dc3f --- /dev/null +++ b/crates/bevy_reflect/src/impls/hashbrown.rs @@ -0,0 +1,43 @@ +use crate::impls::macros::{impl_reflect_for_hashmap, impl_reflect_for_hashset}; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; +use bevy_reflect_derive::impl_type_path; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashmap!(hashbrown::hash_map::HashMap); +impl_type_path!(::hashbrown::hash_map::HashMap); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::hashbrown::hash_map::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +impl_reflect_for_hashset!(::hashbrown::hash_set::HashSet); +impl_type_path!(::hashbrown::hash_set::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::hashbrown::hash_set::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +#[cfg(test)] +mod tests { + use crate::Reflect; + use static_assertions::assert_impl_all; + + #[test] + fn should_reflect_hashmaps() { + // We specify `foldhash::fast::RandomState` directly here since without the `default-hasher` + // feature, hashbrown uses an empty enum to force users to specify their own + assert_impl_all!(hashbrown::HashMap: Reflect); + } +} diff --git a/crates/bevy_reflect/src/impls/macros/list.rs b/crates/bevy_reflect/src/impls/macros/list.rs new file mode 100644 index 0000000000..81a27047cb --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/list.rs @@ -0,0 +1,185 @@ +macro_rules! impl_reflect_for_veclike { + ($ty:ty, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { + const _: () = { + impl $crate::list::List for $ty { + #[inline] + fn get(&self, index: usize) -> Option<&dyn $crate::reflect::PartialReflect> { + <$sub>::get(self, index).map(|value| value as &dyn $crate::reflect::PartialReflect) + } + + #[inline] + fn get_mut(&mut self, index: usize) -> Option<&mut dyn $crate::reflect::PartialReflect> { + <$sub>::get_mut(self, index).map(|value| value as &mut dyn $crate::reflect::PartialReflect) + } + + fn insert(&mut self, index: usize, value: bevy_platform::prelude::Box) { + let value = value.try_take::().unwrap_or_else(|value| { + T::from_reflect(&*value).unwrap_or_else(|| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }) + }); + $insert(self, index, value); + } + + fn remove(&mut self, index: usize) -> bevy_platform::prelude::Box { + bevy_platform::prelude::Box::new($remove(self, index)) + } + + fn push(&mut self, value: bevy_platform::prelude::Box) { + let value = T::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to push invalid value of type {}.", + value.reflect_type_path() + ) + }); + $push(self, value); + } + + fn pop(&mut self) -> Option> { + $pop(self).map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + } + + #[inline] + fn len(&self) -> usize { + <$sub>::len(self) + } + + #[inline] + fn iter(&self) -> $crate::list::ListIter { + $crate::list::ListIter::new(self) + } + + #[inline] + fn drain(&mut self) -> alloc::vec::Vec> { + self.drain(..) + .map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + .collect() + } + } + + impl $crate::reflect::PartialReflect for $ty { + #[inline] + fn get_represented_type_info(&self) -> Option<&'static $crate::type_info::TypeInfo> { + Some(::type_info()) + } + + fn into_partial_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { + self + } + + #[inline] + fn as_partial_reflect(&self) -> &dyn $crate::reflect::PartialReflect { + self + } + + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn $crate::reflect::PartialReflect { + self + } + + fn try_into_reflect( + self: bevy_platform::prelude::Box, + ) -> Result, bevy_platform::prelude::Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn $crate::reflect::Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn $crate::reflect::Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> $crate::kind::ReflectKind { + $crate::kind::ReflectKind::List + } + + fn reflect_ref(&self) -> $crate::kind::ReflectRef { + $crate::kind::ReflectRef::List(self) + } + + fn reflect_mut(&mut self) -> $crate::kind::ReflectMut { + $crate::kind::ReflectMut::List(self) + } + + fn reflect_owned(self: bevy_platform::prelude::Box) -> $crate::kind::ReflectOwned { + $crate::kind::ReflectOwned::List(self) + } + + fn reflect_clone(&self) -> Result, $crate::error::ReflectCloneError> { + Ok(bevy_platform::prelude::Box::new( + self.iter() + .map(|value| value.reflect_clone_and_take()) + .collect::>()?, + )) + } + + fn reflect_hash(&self) -> Option { + $crate::list::list_hash(self) + } + + fn reflect_partial_eq(&self, value: &dyn $crate::reflect::PartialReflect) -> Option { + $crate::list::list_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn $crate::reflect::PartialReflect) { + $crate::list::list_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn $crate::reflect::PartialReflect) -> Result<(), $crate::reflect::ApplyError> { + $crate::list::list_try_apply(self, value) + } + } + + $crate::impl_full_reflect!( for $ty where T: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration); + + impl $crate::type_info::Typed for $ty { + fn type_info() -> &'static $crate::type_info::TypeInfo { + static CELL: $crate::utility::GenericTypeInfoCell = $crate::utility::GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + $crate::type_info::TypeInfo::List( + $crate::list::ListInfo::new::().with_generics($crate::generics::Generics::from_iter([ + $crate::generics::TypeParamInfo::new::("T") + ])) + ) + }) + } + } + + impl $crate::type_registry::GetTypeRegistration + for $ty + { + fn get_type_registration() -> $crate::type_registry::TypeRegistration { + let mut registration = $crate::type_registry::TypeRegistration::of::<$ty>(); + registration.insert::<$crate::type_registry::ReflectFromPtr>($crate::type_registry::FromType::<$ty>::from_type()); + registration.insert::<$crate::from_reflect::ReflectFromReflect>($crate::type_registry::FromType::<$ty>::from_type()); + registration + } + + fn register_type_dependencies(registry: &mut $crate::type_registry::TypeRegistry) { + registry.register::(); + } + } + + impl $crate::from_reflect::FromReflect for $ty { + fn from_reflect(reflect: &dyn $crate::reflect::PartialReflect) -> Option { + let ref_list = reflect.reflect_ref().as_list().ok()?; + + let mut new_list = Self::with_capacity(ref_list.len()); + + for field in ref_list.iter() { + $push(&mut new_list, T::from_reflect(field)?); + } + + Some(new_list) + } + } + }; + }; +} + +pub(crate) use impl_reflect_for_veclike; diff --git a/crates/bevy_reflect/src/impls/macros/map.rs b/crates/bevy_reflect/src/impls/macros/map.rs new file mode 100644 index 0000000000..e87bb314b5 --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/map.rs @@ -0,0 +1,240 @@ +macro_rules! impl_reflect_for_hashmap { + ($ty:path) => { + const _: () = { + impl $crate::map::Map for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get(&self, key: &dyn $crate::reflect::PartialReflect) -> Option<&dyn $crate::reflect::PartialReflect> { + key.try_downcast_ref::() + .and_then(|key| Self::get(self, key)) + .map(|value| value as &dyn $crate::reflect::PartialReflect) + } + + fn get_mut(&mut self, key: &dyn $crate::reflect::PartialReflect) -> Option<&mut dyn $crate::reflect::PartialReflect> { + key.try_downcast_ref::() + .and_then(move |key| Self::get_mut(self, key)) + .map(|value| value as &mut dyn $crate::reflect::PartialReflect) + } + + fn len(&self) -> usize { + Self::len(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)> { + self.drain() + .map(|(key, value)| { + ( + bevy_platform::prelude::Box::new(key) as bevy_platform::prelude::Box, + bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box, + ) + }) + .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)); + for (k, v) in self { + let key = K::from_reflect(k).unwrap_or_else(|| { + panic!( + "Attempted to clone invalid key of type {}.", + k.reflect_type_path() + ) + }); + dynamic_map.insert_boxed(bevy_platform::prelude::Box::new(key), v.to_dynamic()); + } + dynamic_map + } + + fn insert_boxed( + &mut self, + key: bevy_platform::prelude::Box, + value: bevy_platform::prelude::Box, + ) -> Option> { + let key = K::take_from_reflect(key).unwrap_or_else(|key| { + panic!( + "Attempted to insert invalid key of type {}.", + key.reflect_type_path() + ) + }); + let value = V::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.insert(key, value) + .map(|old_value| bevy_platform::prelude::Box::new(old_value) as bevy_platform::prelude::Box) + } + + fn remove(&mut self, key: &dyn $crate::reflect::PartialReflect) -> Option> { + let mut from_reflect = None; + key.try_downcast_ref::() + .or_else(|| { + from_reflect = K::from_reflect(key); + from_reflect.as_ref() + }) + .and_then(|key| self.remove(key)) + .map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + } + } + + impl $crate::reflect::PartialReflect for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get_represented_type_info(&self) -> Option<&'static $crate::type_info::TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { + self + } + + fn as_partial_reflect(&self) -> &dyn $crate::reflect::PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn $crate::reflect::PartialReflect { + self + } + + fn try_into_reflect( + self: bevy_platform::prelude::Box, + ) -> Result, bevy_platform::prelude::Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn $crate::reflect::Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn $crate::reflect::Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> $crate::kind::ReflectKind { + $crate::kind::ReflectKind::Map + } + + fn reflect_ref(&self) -> $crate::kind::ReflectRef { + $crate::kind::ReflectRef::Map(self) + } + + fn reflect_mut(&mut self) -> $crate::kind::ReflectMut { + $crate::kind::ReflectMut::Map(self) + } + + fn reflect_owned(self: bevy_platform::prelude::Box) -> $crate::kind::ReflectOwned { + $crate::kind::ReflectOwned::Map(self) + } + + fn reflect_clone(&self) -> Result, $crate::error::ReflectCloneError> { + let mut map = Self::with_capacity_and_hasher(self.len(), S::default()); + for (key, value) in self.iter() { + let key = key.reflect_clone_and_take()?; + let value = value.reflect_clone_and_take()?; + map.insert(key, value); + } + + Ok(bevy_platform::prelude::Box::new(map)) + } + + fn reflect_partial_eq(&self, value: &dyn $crate::reflect::PartialReflect) -> Option { + $crate::map::map_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn $crate::reflect::PartialReflect) { + $crate::map::map_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn $crate::reflect::PartialReflect) -> Result<(), $crate::reflect::ApplyError> { + $crate::map::map_try_apply(self, value) + } + } + + $crate::impl_full_reflect!( + for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + ); + + impl $crate::type_info::Typed for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn type_info() -> &'static $crate::type_info::TypeInfo { + static CELL: $crate::utility::GenericTypeInfoCell = $crate::utility::GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + $crate::type_info::TypeInfo::Map( + $crate::map::MapInfo::new::().with_generics($crate::generics::Generics::from_iter([ + $crate::generics::TypeParamInfo::new::("K"), + $crate::generics::TypeParamInfo::new::("V"), + ])), + ) + }) + } + } + + impl $crate::type_registry::GetTypeRegistration for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync + Default, + { + fn get_type_registration() -> $crate::type_registry::TypeRegistration { + let mut registration = $crate::type_registry::TypeRegistration::of::(); + registration.insert::<$crate::type_registry::ReflectFromPtr>($crate::type_registry::FromType::::from_type()); + registration.insert::<$crate::from_reflect::ReflectFromReflect>($crate::type_registry::FromType::::from_type()); + registration + } + + fn register_type_dependencies(registry: &mut $crate::type_registry::TypeRegistry) { + registry.register::(); + registry.register::(); + } + } + + impl $crate::from_reflect::FromReflect for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn from_reflect(reflect: &dyn $crate::reflect::PartialReflect) -> Option { + let ref_map = reflect.reflect_ref().as_map().ok()?; + + let mut new_map = Self::with_capacity_and_hasher(ref_map.len(), S::default()); + + for (key, value) in ref_map.iter() { + let new_key = K::from_reflect(key)?; + let new_value = V::from_reflect(value)?; + new_map.insert(new_key, new_value); + } + + Some(new_map) + } + } + }; + }; +} + +pub(crate) use impl_reflect_for_hashmap; diff --git a/crates/bevy_reflect/src/impls/macros/mod.rs b/crates/bevy_reflect/src/impls/macros/mod.rs new file mode 100644 index 0000000000..1e57c39626 --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/mod.rs @@ -0,0 +1,7 @@ +pub(crate) use list::*; +pub(crate) use map::*; +pub(crate) use set::*; + +mod list; +mod map; +mod set; diff --git a/crates/bevy_reflect/src/impls/macros/set.rs b/crates/bevy_reflect/src/impls/macros/set.rs new file mode 100644 index 0000000000..844b904cde --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/set.rs @@ -0,0 +1,207 @@ +macro_rules! impl_reflect_for_hashset { + ($ty:path) => { + const _: () = { + impl $crate::set::Set for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get(&self, value: &dyn $crate::reflect::PartialReflect) -> Option<&dyn $crate::reflect::PartialReflect> { + value + .try_downcast_ref::() + .and_then(|value| Self::get(self, value)) + .map(|value| value as &dyn $crate::reflect::PartialReflect) + } + + fn len(&self) -> usize { + Self::len(self) + } + + fn iter(&self) -> bevy_platform::prelude::Box + '_> { + let iter = self.iter().map(|v| v as &dyn $crate::reflect::PartialReflect); + bevy_platform::prelude::Box::new(iter) + } + + fn drain(&mut self) -> bevy_platform::prelude::Vec> { + self.drain() + .map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + .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!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.insert(value) + } + + fn remove(&mut self, value: &dyn $crate::reflect::PartialReflect) -> bool { + let mut from_reflect = None; + value + .try_downcast_ref::() + .or_else(|| { + from_reflect = V::from_reflect(value); + from_reflect.as_ref() + }) + .is_some_and(|value| self.remove(value)) + } + + fn contains(&self, value: &dyn $crate::reflect::PartialReflect) -> bool { + let mut from_reflect = None; + value + .try_downcast_ref::() + .or_else(|| { + from_reflect = V::from_reflect(value); + from_reflect.as_ref() + }) + .is_some_and(|value| self.contains(value)) + } + } + + impl $crate::reflect::PartialReflect for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get_represented_type_info(&self) -> Option<&'static $crate::type_info::TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { + self + } + + fn as_partial_reflect(&self) -> &dyn $crate::reflect::PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn $crate::reflect::PartialReflect { + self + } + + #[inline] + fn try_into_reflect( + self: bevy_platform::prelude::Box, + ) -> Result, bevy_platform::prelude::Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn $crate::reflect::Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn $crate::reflect::Reflect> { + Some(self) + } + + fn apply(&mut self, value: &dyn $crate::reflect::PartialReflect) { + $crate::set::set_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn $crate::reflect::PartialReflect) -> Result<(), $crate::reflect::ApplyError> { + $crate::set::set_try_apply(self, value) + } + + fn reflect_kind(&self) -> $crate::kind::ReflectKind { + $crate::kind::ReflectKind::Set + } + + fn reflect_ref(&self) -> $crate::kind::ReflectRef { + $crate::kind::ReflectRef::Set(self) + } + + fn reflect_mut(&mut self) -> $crate::kind::ReflectMut { + $crate::kind::ReflectMut::Set(self) + } + + fn reflect_owned(self: bevy_platform::prelude::Box) -> $crate::kind::ReflectOwned { + $crate::kind::ReflectOwned::Set(self) + } + + fn reflect_clone(&self) -> Result, $crate::error::ReflectCloneError> { + let mut set = Self::with_capacity_and_hasher(self.len(), S::default()); + for value in self.iter() { + let value = value.reflect_clone_and_take()?; + set.insert(value); + } + + Ok(bevy_platform::prelude::Box::new(set)) + } + + fn reflect_partial_eq(&self, value: &dyn $crate::reflect::PartialReflect) -> Option { + $crate::set::set_partial_eq(self, value) + } + } + + impl $crate::type_info::Typed for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn type_info() -> &'static $crate::type_info::TypeInfo { + static CELL: $crate::utility::GenericTypeInfoCell = $crate::utility::GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + $crate::type_info::TypeInfo::Set( + $crate::set::SetInfo::new::().with_generics($crate::generics::Generics::from_iter([ + $crate::generics::TypeParamInfo::new::("V") + ])) + ) + }) + } + } + + impl $crate::type_registry::GetTypeRegistration for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync + Default, + { + fn get_type_registration() -> $crate::type_registry::TypeRegistration { + let mut registration = $crate::type_registry::TypeRegistration::of::(); + registration.insert::<$crate::type_registry::ReflectFromPtr>($crate::type_registry::FromType::::from_type()); + registration.insert::<$crate::from_reflect::ReflectFromReflect>($crate::type_registry::FromType::::from_type()); + registration + } + + fn register_type_dependencies(registry: &mut $crate::type_registry::TypeRegistry) { + registry.register::(); + } + } + + $crate::impl_full_reflect!( + for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + ); + + impl $crate::from_reflect::FromReflect for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn from_reflect(reflect: &dyn $crate::reflect::PartialReflect) -> Option { + let ref_set = reflect.reflect_ref().as_set().ok()?; + + let mut new_set = Self::with_capacity_and_hasher(ref_set.len(), S::default()); + + for value in ref_set.iter() { + let new_value = V::from_reflect(value)?; + new_set.insert(new_value); + } + + Some(new_set) + } + } + }; + }; +} + +pub(crate) use impl_reflect_for_hashset; diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index 942bcbe83f..86b7284381 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -4,7 +4,7 @@ use crate::{ ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypeParamInfo, TypePath, TypeRegistration, Typed, }; -use alloc::{borrow::Cow, boxed::Box, string::ToString, vec::Vec}; +use alloc::{boxed::Box, vec::Vec}; use bevy_reflect::ReflectCloneError; use bevy_reflect_derive::impl_type_path; use core::any::Any; @@ -77,6 +77,7 @@ where .collect() } } + impl PartialReflect for SmallVec where T::Item: FromReflect + MaybeTyped + TypePath, @@ -136,16 +137,11 @@ where fn reflect_clone(&self) -> Result, ReflectCloneError> { Ok(Box::new( - self.iter() - .map(|value| { - value - .reflect_clone()? - .take() - .map_err(|_| ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - }) - }) + // `(**self)` avoids getting `SmallVec as List::iter`, which + // would give us the wrong item type. + (**self) + .iter() + .map(PartialReflect::reflect_clone_and_take) .collect::>()?, )) } diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs deleted file mode 100644 index 0115afbf79..0000000000 --- a/crates/bevy_reflect/src/impls/std.rs +++ /dev/null @@ -1,2873 +0,0 @@ -#![expect( - unused_qualifications, - reason = "Temporary workaround for impl_reflect!(Option/Result false-positive" -)] - -use crate::{ - impl_type_path, map_apply, map_partial_eq, map_try_apply, - prelude::ReflectDefault, - reflect::impl_full_reflect, - set_apply, set_partial_eq, set_try_apply, - utility::{reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell}, - ApplyError, Array, ArrayInfo, ArrayIter, DynamicMap, DynamicTypePath, FromReflect, FromType, - Generics, GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, MaybeTyped, - OpaqueInfo, PartialReflect, Reflect, ReflectCloneError, ReflectDeserialize, ReflectFromPtr, - ReflectFromReflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, ReflectSerialize, Set, - SetInfo, TypeInfo, TypeParamInfo, TypePath, TypeRegistration, TypeRegistry, Typed, -}; -use alloc::{ - borrow::{Cow, ToOwned}, - boxed::Box, - collections::VecDeque, - format, - string::ToString, - vec::Vec, -}; -use bevy_reflect_derive::{impl_reflect, impl_reflect_opaque}; -use core::{ - any::Any, - fmt, - hash::{BuildHasher, Hash, Hasher}, - panic::Location, -}; - -#[cfg(feature = "std")] -use std::path::Path; - -impl_reflect_opaque!(bool( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(char( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(usize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(isize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(f32( - Clone, - Debug, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(f64( - Clone, - Debug, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_type_path!(str); -impl_reflect_opaque!(::alloc::string::String( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -#[cfg(feature = "std")] -impl_reflect_opaque!(::std::path::PathBuf( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(::core::any::TypeId(Clone, Debug, Hash, PartialEq,)); -impl_reflect_opaque!(::alloc::collections::BTreeSet(Clone)); -impl_reflect_opaque!(::core::ops::Range(Clone)); -impl_reflect_opaque!(::core::ops::RangeInclusive(Clone)); -impl_reflect_opaque!(::core::ops::RangeFrom(Clone)); -impl_reflect_opaque!(::core::ops::RangeTo(Clone)); -impl_reflect_opaque!(::core::ops::RangeToInclusive(Clone)); -impl_reflect_opaque!(::core::ops::RangeFull(Clone)); -impl_reflect_opaque!(::core::ops::Bound(Clone)); -impl_reflect_opaque!(::core::time::Duration( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(::bevy_platform::time::Instant( - Clone, Debug, Hash, PartialEq -)); -impl_reflect_opaque!(::core::num::NonZeroI128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroIsize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroUsize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::Wrapping(Clone)); -impl_reflect_opaque!(::core::num::Saturating(Clone)); -impl_reflect_opaque!(::bevy_platform::sync::Arc(Clone)); - -// `Serialize` and `Deserialize` only for platforms supported by serde: -// https://github.com/serde-rs/serde/blob/3ffb86fc70efd3d329519e2dddfa306cc04f167c/serde/src/de/impls.rs#L1732 -#[cfg(all(any(unix, windows), feature = "std"))] -impl_reflect_opaque!(::std::ffi::OsString( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -#[cfg(all(not(any(unix, windows)), feature = "std"))] -impl_reflect_opaque!(::std::ffi::OsString(Clone, Debug, Hash, PartialEq)); -impl_reflect_opaque!(::alloc::collections::BinaryHeap(Clone)); - -macro_rules! impl_reflect_for_atomic { - ($ty:ty, $ordering:expr) => { - impl_type_path!($ty); - - const _: () = { - #[cfg(feature = "functions")] - crate::func::macros::impl_function_traits!($ty); - - impl GetTypeRegistration for $ty - where - $ty: Any + Send + Sync, - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - - // Serde only supports atomic types when the "std" feature is enabled - #[cfg(feature = "std")] - { - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - } - - registration - } - } - - impl Typed for $ty - where - $ty: Any + Send + Sync, - { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| { - let info = OpaqueInfo::new::(); - TypeInfo::Opaque(info) - }) - } - } - - impl PartialReflect for $ty - where - $ty: Any + Send + Sync, - { - #[inline] - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - #[inline] - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - #[inline] - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - #[inline] - fn try_into_reflect( - self: Box, - ) -> Result, Box> { - Ok(self) - } - #[inline] - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - #[inline] - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - #[inline] - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(<$ty>::new(self.load($ordering)))) - } - - #[inline] - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - *self = <$ty>::new(value.load($ordering)); - } else { - return Err(ApplyError::MismatchedTypes { - from_type: Into::into(DynamicTypePath::reflect_type_path(value)), - to_type: Into::into(::type_path()), - }); - } - Ok(()) - } - #[inline] - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - #[inline] - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - #[inline] - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - #[inline] - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(self, f) - } - } - - impl FromReflect for $ty - where - $ty: Any + Send + Sync, - { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(<$ty>::new( - reflect.try_downcast_ref::<$ty>()?.load($ordering), - )) - } - } - }; - - impl_full_reflect!(for $ty where $ty: Any + Send + Sync); - }; -} - -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicIsize, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicUsize, - ::core::sync::atomic::Ordering::SeqCst -); -#[cfg(target_has_atomic = "64")] -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI64, - ::core::sync::atomic::Ordering::SeqCst -); -#[cfg(target_has_atomic = "64")] -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU64, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI32, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU32, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI16, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU16, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI8, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU8, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicBool, - ::core::sync::atomic::Ordering::SeqCst -); - -macro_rules! impl_reflect_for_veclike { - ($ty:ty, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { - impl List for $ty { - #[inline] - fn get(&self, index: usize) -> Option<&dyn PartialReflect> { - <$sub>::get(self, index).map(|value| value as &dyn PartialReflect) - } - - #[inline] - fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - <$sub>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) - } - - fn insert(&mut self, index: usize, value: Box) { - let value = value.try_take::().unwrap_or_else(|value| { - T::from_reflect(&*value).unwrap_or_else(|| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }) - }); - $insert(self, index, value); - } - - fn remove(&mut self, index: usize) -> Box { - Box::new($remove(self, index)) - } - - fn push(&mut self, value: Box) { - let value = T::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to push invalid value of type {}.", - value.reflect_type_path() - ) - }); - $push(self, value); - } - - fn pop(&mut self) -> Option> { - $pop(self).map(|value| Box::new(value) as Box) - } - - #[inline] - fn len(&self) -> usize { - <$sub>::len(self) - } - - #[inline] - fn iter(&self) -> ListIter { - ListIter::new(self) - } - - #[inline] - fn drain(&mut self) -> Vec> { - self.drain(..) - .map(|value| Box::new(value) as Box) - .collect() - } - } - - impl PartialReflect for $ty { - #[inline] - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - fn into_partial_reflect(self: Box) -> Box { - self - } - - #[inline] - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - #[inline] - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect( - self: Box, - ) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::List - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::List(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::List(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::List(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new( - self.iter() - .map(|value| { - value.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - } - }) - }) - .collect::>()?, - )) - } - - fn reflect_hash(&self) -> Option { - crate::list_hash(self) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - crate::list_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - crate::list_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - crate::list_try_apply(self, value) - } - } - - impl_full_reflect!( for $ty where T: FromReflect + MaybeTyped + TypePath + GetTypeRegistration); - - impl Typed for $ty { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::List( - ListInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("T") - ])) - ) - }) - } - } - - impl GetTypeRegistration - for $ty - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::<$ty>(); - registration.insert::(FromType::<$ty>::from_type()); - registration.insert::(FromType::<$ty>::from_type()); - registration - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } - } - - impl FromReflect for $ty { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_list = reflect.reflect_ref().as_list().ok()?; - - let mut new_list = Self::with_capacity(ref_list.len()); - - for field in ref_list.iter() { - $push(&mut new_list, T::from_reflect(field)?); - } - - Some(new_list) - } - } - }; -} - -impl_reflect_for_veclike!(Vec, Vec::insert, Vec::remove, Vec::push, Vec::pop, [T]); -impl_type_path!(::alloc::vec::Vec); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(Vec; ); - -impl_reflect_for_veclike!( - VecDeque, - VecDeque::insert, - VecDeque::remove, - VecDeque::push_back, - VecDeque::pop_back, - VecDeque:: -); -impl_type_path!(::alloc::collections::VecDeque); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(VecDeque; ); - -macro_rules! impl_reflect_for_hashmap { - ($ty:path) => { - impl Map for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(|key| Self::get(self, key)) - .map(|value| value as &dyn PartialReflect) - } - - fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(move |key| Self::get_mut(self, key)) - .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 drain(&mut self) -> Vec<(Box, Box)> { - self.drain() - .map(|(key, value)| { - ( - Box::new(key) as Box, - Box::new(value) as Box, - ) - }) - .collect() - } - - fn to_dynamic_map(&self) -> DynamicMap { - let mut dynamic_map = DynamicMap::default(); - dynamic_map.set_represented_type(self.get_represented_type_info()); - for (k, v) in self { - let key = K::from_reflect(k).unwrap_or_else(|| { - panic!( - "Attempted to clone invalid key of type {}.", - k.reflect_type_path() - ) - }); - dynamic_map.insert_boxed(Box::new(key), v.to_dynamic()); - } - dynamic_map - } - - fn insert_boxed( - &mut self, - key: Box, - value: Box, - ) -> Option> { - let key = K::take_from_reflect(key).unwrap_or_else(|key| { - panic!( - "Attempted to insert invalid key of type {}.", - key.reflect_type_path() - ) - }); - let value = V::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.insert(key, value) - .map(|old_value| Box::new(old_value) as Box) - } - - fn remove(&mut self, key: &dyn PartialReflect) -> Option> { - let mut from_reflect = None; - key.try_downcast_ref::() - .or_else(|| { - from_reflect = K::from_reflect(key); - from_reflect.as_ref() - }) - .and_then(|key| self.remove(key)) - .map(|value| Box::new(value) as Box) - } - } - - impl PartialReflect for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect( - self: Box, - ) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Map - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Map(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Map(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Map(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - let mut map = Self::with_capacity_and_hasher(self.len(), S::default()); - for (key, value) in self.iter() { - let key = key.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(key.reflect_type_path().to_string()), - } - })?; - let value = value.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - } - })?; - map.insert(key, value); - } - - Ok(Box::new(map)) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - map_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - map_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - map_try_apply(self, value) - } - } - - impl_full_reflect!( - for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - ); - - impl Typed for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::Map( - MapInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("K"), - TypeParamInfo::new::("V"), - ])), - ) - }) - } - } - - impl GetTypeRegistration for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync + Default, - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - registry.register::(); - } - } - - impl FromReflect for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_map = reflect.reflect_ref().as_map().ok()?; - - let mut new_map = Self::with_capacity_and_hasher(ref_map.len(), S::default()); - - for (key, value) in ref_map.iter() { - let new_key = K::from_reflect(key)?; - let new_value = V::from_reflect(value)?; - new_map.insert(new_key, new_value); - } - - Some(new_map) - } - } - }; -} - -#[cfg(feature = "std")] -impl_reflect_for_hashmap!(::std::collections::HashMap); -impl_type_path!(::core::hash::BuildHasherDefault); -#[cfg(feature = "std")] -impl_type_path!(::std::collections::hash_map::RandomState); -#[cfg(feature = "std")] -impl_type_path!(::std::collections::HashMap); -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(::std::collections::HashMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -impl_reflect_for_hashmap!(bevy_platform::collections::HashMap); -impl_type_path!(::bevy_platform::collections::HashMap); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -#[cfg(feature = "hashbrown")] -impl_reflect_for_hashmap!(hashbrown::hash_map::HashMap); -#[cfg(feature = "hashbrown")] -impl_type_path!(::hashbrown::hash_map::HashMap); -#[cfg(all(feature = "functions", feature = "hashbrown"))] -crate::func::macros::impl_function_traits!(::hashbrown::hash_map::HashMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -macro_rules! impl_reflect_for_hashset { - ($ty:path) => { - impl Set for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get(&self, value: &dyn PartialReflect) -> Option<&dyn PartialReflect> { - value - .try_downcast_ref::() - .and_then(|value| Self::get(self, value)) - .map(|value| value as &dyn PartialReflect) - } - - fn len(&self) -> usize { - Self::len(self) - } - - fn iter(&self) -> Box + '_> { - let iter = self.iter().map(|v| v as &dyn PartialReflect); - Box::new(iter) - } - - fn drain(&mut self) -> Vec> { - self.drain() - .map(|value| Box::new(value) as Box) - .collect() - } - - fn insert_boxed(&mut self, value: Box) -> bool { - let value = V::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.insert(value) - } - - fn remove(&mut self, value: &dyn PartialReflect) -> bool { - let mut from_reflect = None; - value - .try_downcast_ref::() - .or_else(|| { - from_reflect = V::from_reflect(value); - from_reflect.as_ref() - }) - .is_some_and(|value| self.remove(value)) - } - - fn contains(&self, value: &dyn PartialReflect) -> bool { - let mut from_reflect = None; - value - .try_downcast_ref::() - .or_else(|| { - from_reflect = V::from_reflect(value); - from_reflect.as_ref() - }) - .is_some_and(|value| self.contains(value)) - } - } - - impl PartialReflect for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - #[inline] - fn try_into_reflect( - self: Box, - ) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - set_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - set_try_apply(self, value) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Set - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Set(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Set(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Set(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - let mut set = Self::with_capacity_and_hasher(self.len(), S::default()); - for value in self.iter() { - let value = value.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - } - })?; - set.insert(value); - } - - Ok(Box::new(set)) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - set_partial_eq(self, value) - } - } - - impl Typed for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::Set( - SetInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("V") - ])) - ) - }) - } - } - - impl GetTypeRegistration for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync + Default, - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } - } - - impl_full_reflect!( - for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - ); - - impl FromReflect for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_set = reflect.reflect_ref().as_set().ok()?; - - let mut new_set = Self::with_capacity_and_hasher(ref_set.len(), S::default()); - - for value in ref_set.iter() { - let new_value = V::from_reflect(value)?; - new_set.insert(new_value); - } - - Some(new_set) - } - } - }; -} - -impl_type_path!(::bevy_platform::hash::NoOpHash); -impl_type_path!(::bevy_platform::hash::FixedHasher); -impl_type_path!(::bevy_platform::hash::PassHash); -impl_reflect_opaque!(::core::net::SocketAddr( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); - -#[cfg(feature = "std")] -impl_reflect_for_hashset!(::std::collections::HashSet); -#[cfg(feature = "std")] -impl_type_path!(::std::collections::HashSet); -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(::std::collections::HashSet; - < - V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -impl_reflect_for_hashset!(::bevy_platform::collections::HashSet); -impl_type_path!(::bevy_platform::collections::HashSet); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashSet; - < - V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -#[cfg(feature = "hashbrown")] -impl_reflect_for_hashset!(::hashbrown::hash_set::HashSet); -#[cfg(feature = "hashbrown")] -impl_type_path!(::hashbrown::hash_set::HashSet); -#[cfg(all(feature = "functions", feature = "hashbrown"))] -crate::func::macros::impl_function_traits!(::hashbrown::hash_set::HashSet; - < - V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -impl Map for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(|key| Self::get(self, key)) - .map(|value| value as &dyn PartialReflect) - } - - fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(move |key| Self::get_mut(self, key)) - .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 drain(&mut self) -> Vec<(Box, Box)> { - // BTreeMap doesn't have a `drain` function. See - // https://github.com/rust-lang/rust/issues/81074. So we have to fake one by popping - // elements off one at a time. - let mut result = Vec::with_capacity(self.len()); - while let Some((k, v)) = self.pop_first() { - result.push(( - Box::new(k) as Box, - Box::new(v) as Box, - )); - } - result - } - - fn insert_boxed( - &mut self, - key: Box, - value: Box, - ) -> Option> { - let key = K::take_from_reflect(key).unwrap_or_else(|key| { - panic!( - "Attempted to insert invalid key of type {}.", - key.reflect_type_path() - ) - }); - let value = V::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.insert(key, value) - .map(|old_value| Box::new(old_value) as Box) - } - - fn remove(&mut self, key: &dyn PartialReflect) -> Option> { - let mut from_reflect = None; - key.try_downcast_ref::() - .or_else(|| { - from_reflect = K::from_reflect(key); - from_reflect.as_ref() - }) - .and_then(|key| self.remove(key)) - .map(|value| Box::new(value) as Box) - } -} - -impl PartialReflect for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - #[inline] - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Map - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Map(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Map(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Map(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - let mut map = Self::new(); - for (key, value) in self.iter() { - let key = - key.reflect_clone()? - .take() - .map_err(|_| ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(key.reflect_type_path().to_string()), - })?; - let value = - value - .reflect_clone()? - .take() - .map_err(|_| ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - })?; - map.insert(key, value); - } - - Ok(Box::new(map)) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - map_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - map_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - map_try_apply(self, value) - } -} - -impl_full_reflect!( - for ::alloc::collections::BTreeMap - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -); - -impl Typed for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::Map( - MapInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("K"), - TypeParamInfo::new::("V"), - ])), - ) - }) - } -} - -impl GetTypeRegistration for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_map = reflect.reflect_ref().as_map().ok()?; - - let mut new_map = Self::new(); - - for (key, value) in ref_map.iter() { - let new_key = K::from_reflect(key)?; - let new_value = V::from_reflect(value)?; - new_map.insert(new_key, new_value); - } - - Some(new_map) - } -} - -impl_type_path!(::alloc::collections::BTreeMap); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(::alloc::collections::BTreeMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration - > -); - -impl Array for [T; N] { - #[inline] - fn get(&self, index: usize) -> Option<&dyn PartialReflect> { - <[T]>::get(self, index).map(|value| value as &dyn PartialReflect) - } - - #[inline] - fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - <[T]>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) - } - - #[inline] - fn len(&self) -> usize { - N - } - - #[inline] - fn iter(&self) -> ArrayIter { - ArrayIter::new(self) - } - - #[inline] - fn drain(self: Box) -> Vec> { - self.into_iter() - .map(|value| Box::new(value) as Box) - .collect() - } -} - -impl PartialReflect - for [T; N] -{ - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - #[inline] - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Array - } - - #[inline] - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Array(self) - } - - #[inline] - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Array(self) - } - - #[inline] - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Array(self) - } - - #[inline] - fn reflect_hash(&self) -> Option { - crate::array_hash(self) - } - - #[inline] - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - crate::array_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - crate::array_apply(self, value); - } - - #[inline] - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - crate::array_try_apply(self, value) - } -} - -impl Reflect for [T; N] { - #[inline] - fn into_any(self: Box) -> Box { - self - } - - #[inline] - fn as_any(&self) -> &dyn Any { - self - } - - #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - #[inline] - fn into_reflect(self: Box) -> Box { - self - } - - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self - } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -impl FromReflect - for [T; N] -{ - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_array = reflect.reflect_ref().as_array().ok()?; - - let mut temp_vec = Vec::with_capacity(ref_array.len()); - - for field in ref_array.iter() { - temp_vec.push(T::from_reflect(field)?); - } - - temp_vec.try_into().ok() - } -} - -impl Typed for [T; N] { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| TypeInfo::Array(ArrayInfo::new::(N))) - } -} - -impl TypePath for [T; N] { - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::short_type_path())) - } -} - -impl GetTypeRegistration - for [T; N] -{ - fn get_type_registration() -> TypeRegistration { - TypeRegistration::of::<[T; N]>() - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!([T; N]; [const N: usize]); - -impl_reflect! { - #[type_path = "core::option"] - enum Option { - None, - Some(T), - } -} - -impl_reflect! { - #[type_path = "core::result"] - enum Result { - Ok(T), - Err(E), - } -} - -impl TypePath for &'static T { - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&{}", T::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&{}", T::short_type_path())) - } -} - -impl TypePath for &'static mut T { - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&mut {}", T::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&mut {}", T::short_type_path())) - } -} - -impl PartialReflect for Cow<'static, str> { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(self.clone())) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn debug(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { - fmt::Debug::fmt(self, f) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - } else { - return Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static - to_type: Self::type_path().into(), - }); - } - Ok(()) - } -} - -impl_full_reflect!(for Cow<'static, str>); - -impl Typed for Cow<'static, str> { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -impl GetTypeRegistration for Cow<'static, str> { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::>(); - registration.insert::(FromType::>::from_type()); - registration.insert::(FromType::>::from_type()); - registration.insert::(FromType::>::from_type()); - registration.insert::(FromType::>::from_type()); - registration - } -} - -impl FromReflect for Cow<'static, str> { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(reflect.try_downcast_ref::>()?.clone()) - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(Cow<'static, str>); - -impl TypePath for [T] -where - [T]: ToOwned, -{ - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{}]", ::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{}]", ::short_type_path())) - } -} - -impl List - for Cow<'static, [T]> -{ - fn get(&self, index: usize) -> Option<&dyn PartialReflect> { - self.as_ref().get(index).map(|x| x as &dyn PartialReflect) - } - - fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - self.to_mut() - .get_mut(index) - .map(|x| x as &mut dyn PartialReflect) - } - - fn insert(&mut self, index: usize, element: Box) { - let value = T::take_from_reflect(element).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ); - }); - self.to_mut().insert(index, value); - } - - fn remove(&mut self, index: usize) -> Box { - Box::new(self.to_mut().remove(index)) - } - - fn push(&mut self, value: Box) { - let value = T::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to push invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.to_mut().push(value); - } - - fn pop(&mut self) -> Option> { - self.to_mut() - .pop() - .map(|value| Box::new(value) as Box) - } - - fn len(&self) -> usize { - self.as_ref().len() - } - - fn iter(&self) -> ListIter { - ListIter::new(self) - } - - fn drain(&mut self) -> Vec> { - self.to_mut() - .drain(..) - .map(|value| Box::new(value) as Box) - .collect() - } -} - -impl PartialReflect - for Cow<'static, [T]> -{ - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::List - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::List(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::List(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::List(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(self.clone())) - } - - fn reflect_hash(&self) -> Option { - crate::list_hash(self) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - crate::list_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - crate::list_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - crate::list_try_apply(self, value) - } -} - -impl_full_reflect!( - for Cow<'static, [T]> - where - T: FromReflect + Clone + MaybeTyped + TypePath + GetTypeRegistration, -); - -impl Typed - for Cow<'static, [T]> -{ - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| TypeInfo::List(ListInfo::new::())) - } -} - -impl GetTypeRegistration - for Cow<'static, [T]> -{ - fn get_type_registration() -> TypeRegistration { - TypeRegistration::of::>() - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } -} - -impl FromReflect - for Cow<'static, [T]> -{ - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_list = reflect.reflect_ref().as_list().ok()?; - - let mut temp_vec = Vec::with_capacity(ref_list.len()); - - for field in ref_list.iter() { - temp_vec.push(T::from_reflect(field)?); - } - - Some(temp_vec.into()) - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(Cow<'static, [T]>; ); - -impl PartialReflect for &'static str { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(*self)) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self, f) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - } else { - return Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: Self::type_path().into(), - }); - } - Ok(()) - } -} - -impl Reflect for &'static str { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -impl Typed for &'static str { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -impl GetTypeRegistration for &'static str { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for &'static str { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - reflect.try_downcast_ref::().copied() - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(&'static str); - -#[cfg(feature = "std")] -impl PartialReflect for &'static Path { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(*self)) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } -} - -#[cfg(feature = "std")] -impl Reflect for &'static Path { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -#[cfg(feature = "std")] -impl Typed for &'static Path { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -#[cfg(feature = "std")] -impl GetTypeRegistration for &'static Path { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -#[cfg(feature = "std")] -impl FromReflect for &'static Path { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - reflect.try_downcast_ref::().copied() - } -} - -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(&'static Path); - -#[cfg(feature = "std")] -impl PartialReflect for Cow<'static, Path> { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(self.clone())) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self, f) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } -} - -#[cfg(feature = "std")] -impl Reflect for Cow<'static, Path> { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -#[cfg(feature = "std")] -impl Typed for Cow<'static, Path> { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -#[cfg(feature = "std")] -impl_type_path!(::std::path::Path); -impl_type_path!(::alloc::borrow::Cow<'a: 'static, T: ToOwned + ?Sized>); - -#[cfg(feature = "std")] -impl FromReflect for Cow<'static, Path> { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(reflect.try_downcast_ref::()?.clone()) - } -} - -#[cfg(feature = "std")] -impl GetTypeRegistration for Cow<'static, Path> { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(Cow<'static, Path>); - -impl TypePath for &'static Location<'static> { - fn type_path() -> &'static str { - "core::panic::Location" - } - - fn short_type_path() -> &'static str { - "Location" - } -} - -impl PartialReflect for &'static Location<'static> { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(*self)) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } -} - -impl Reflect for &'static Location<'static> { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -impl Typed for &'static Location<'static> { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -impl GetTypeRegistration for &'static Location<'static> { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for &'static Location<'static> { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - reflect.try_downcast_ref::().copied() - } -} - -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(&'static Location<'static>); - -#[cfg(test)] -mod tests { - use crate::{ - Enum, FromReflect, PartialReflect, Reflect, ReflectSerialize, TypeInfo, TypeRegistry, - Typed, VariantInfo, VariantType, - }; - use alloc::{collections::BTreeMap, string::String, vec}; - use bevy_platform::collections::HashMap; - use bevy_platform::time::Instant; - use core::{ - f32::consts::{PI, TAU}, - time::Duration, - }; - use static_assertions::assert_impl_all; - use std::path::Path; - - #[test] - fn can_serialize_duration() { - let mut type_registry = TypeRegistry::default(); - type_registry.register::(); - - let reflect_serialize = type_registry - .get_type_data::(core::any::TypeId::of::()) - .unwrap(); - let _serializable = reflect_serialize.get_serializable(&Duration::ZERO); - } - - #[test] - fn should_partial_eq_char() { - let a: &dyn PartialReflect = &'x'; - let b: &dyn PartialReflect = &'x'; - let c: &dyn PartialReflect = &'o'; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_i32() { - let a: &dyn PartialReflect = &123_i32; - let b: &dyn PartialReflect = &123_i32; - let c: &dyn PartialReflect = &321_i32; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_f32() { - let a: &dyn PartialReflect = &PI; - let b: &dyn PartialReflect = &PI; - let c: &dyn PartialReflect = &TAU; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_string() { - let a: &dyn PartialReflect = &String::from("Hello"); - let b: &dyn PartialReflect = &String::from("Hello"); - let c: &dyn PartialReflect = &String::from("World"); - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_vec() { - let a: &dyn PartialReflect = &vec![1, 2, 3]; - let b: &dyn PartialReflect = &vec![1, 2, 3]; - let c: &dyn PartialReflect = &vec![3, 2, 1]; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_hash_map() { - let mut a = >::default(); - a.insert(0usize, 1.23_f64); - let b = a.clone(); - let mut c = >::default(); - c.insert(0usize, 3.21_f64); - - let a: &dyn PartialReflect = &a; - let b: &dyn PartialReflect = &b; - let c: &dyn PartialReflect = &c; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_btree_map() { - let mut a = BTreeMap::new(); - a.insert(0usize, 1.23_f64); - let b = a.clone(); - let mut c = BTreeMap::new(); - c.insert(0usize, 3.21_f64); - - let a: &dyn Reflect = &a; - let b: &dyn Reflect = &b; - let c: &dyn Reflect = &c; - assert!(a - .reflect_partial_eq(b.as_partial_reflect()) - .unwrap_or_default()); - assert!(!a - .reflect_partial_eq(c.as_partial_reflect()) - .unwrap_or_default()); - } - - #[test] - fn should_partial_eq_option() { - let a: &dyn PartialReflect = &Some(123); - let b: &dyn PartialReflect = &Some(123); - assert_eq!(Some(true), a.reflect_partial_eq(b)); - } - - #[test] - fn option_should_impl_enum() { - assert_impl_all!(Option<()>: Enum); - - let mut value = Some(123usize); - - assert!(value - .reflect_partial_eq(&Some(123usize)) - .unwrap_or_default()); - assert!(!value - .reflect_partial_eq(&Some(321usize)) - .unwrap_or_default()); - - assert_eq!("Some", value.variant_name()); - assert_eq!("core::option::Option::Some", value.variant_path()); - - if value.is_variant(VariantType::Tuple) { - if let Some(field) = value - .field_at_mut(0) - .and_then(|field| field.try_downcast_mut::()) - { - *field = 321; - } - } else { - panic!("expected `VariantType::Tuple`"); - } - - assert_eq!(Some(321), value); - } - - #[test] - fn option_should_from_reflect() { - #[derive(Reflect, PartialEq, Debug)] - struct Foo(usize); - - let expected = Some(Foo(123)); - let output = as FromReflect>::from_reflect(&expected).unwrap(); - - assert_eq!(expected, output); - } - - #[test] - fn option_should_apply() { - #[derive(Reflect, PartialEq, Debug)] - struct Foo(usize); - - // === None on None === // - let patch = None::; - let mut value = None::; - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "None apply onto None"); - - // === Some on None === // - let patch = Some(Foo(123)); - let mut value = None::; - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "Some apply onto None"); - - // === None on Some === // - let patch = None::; - let mut value = Some(Foo(321)); - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "None apply onto Some"); - - // === Some on Some === // - let patch = Some(Foo(123)); - let mut value = Some(Foo(321)); - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "Some apply onto Some"); - } - - #[test] - fn option_should_impl_typed() { - assert_impl_all!(Option<()>: Typed); - - type MyOption = Option; - let info = MyOption::type_info(); - if let TypeInfo::Enum(info) = info { - assert_eq!( - "None", - info.variant_at(0).unwrap().name(), - "Expected `None` to be variant at index `0`" - ); - assert_eq!( - "Some", - info.variant_at(1).unwrap().name(), - "Expected `Some` to be variant at index `1`" - ); - assert_eq!("Some", info.variant("Some").unwrap().name()); - if let VariantInfo::Tuple(variant) = info.variant("Some").unwrap() { - assert!( - variant.field_at(0).unwrap().is::(), - "Expected `Some` variant to contain `i32`" - ); - assert!( - variant.field_at(1).is_none(), - "Expected `Some` variant to only contain 1 field" - ); - } else { - panic!("Expected `VariantInfo::Tuple`"); - } - } else { - panic!("Expected `TypeInfo::Enum`"); - } - } - - #[test] - fn nonzero_usize_impl_reflect_from_reflect() { - let a: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); - let b: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - let forty_two: core::num::NonZero = FromReflect::from_reflect(a).unwrap(); - assert_eq!(forty_two, core::num::NonZero::::new(42).unwrap()); - } - - #[test] - fn instant_should_from_reflect() { - let expected = Instant::now(); - let output = ::from_reflect(&expected).unwrap(); - assert_eq!(expected, output); - } - - #[test] - fn path_should_from_reflect() { - let path = Path::new("hello_world.rs"); - let output = <&'static Path as FromReflect>::from_reflect(&path).unwrap(); - assert_eq!(path, output); - } - - #[test] - fn type_id_should_from_reflect() { - let type_id = core::any::TypeId::of::(); - let output = ::from_reflect(&type_id).unwrap(); - assert_eq!(type_id, output); - } - - #[test] - fn static_str_should_from_reflect() { - let expected = "Hello, World!"; - let output = <&'static str as FromReflect>::from_reflect(&expected).unwrap(); - assert_eq!(expected, output); - } - - #[test] - fn should_reflect_hashmaps() { - assert_impl_all!(std::collections::HashMap: Reflect); - assert_impl_all!(bevy_platform::collections::HashMap: Reflect); - - // We specify `foldhash::fast::RandomState` directly here since without the `default-hasher` - // feature, hashbrown uses an empty enum to force users to specify their own - #[cfg(feature = "hashbrown")] - assert_impl_all!(hashbrown::HashMap: Reflect); - } -} diff --git a/crates/bevy_reflect/src/impls/std/collections/hash_map.rs b/crates/bevy_reflect/src/impls/std/collections/hash_map.rs new file mode 100644 index 0000000000..604c29f517 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/collections/hash_map.rs @@ -0,0 +1,34 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashmap; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashmap!(::std::collections::HashMap); +impl_type_path!(::std::collections::hash_map::RandomState); +impl_type_path!(::std::collections::HashMap); + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::std::collections::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +#[cfg(test)] +mod tests { + use crate::Reflect; + use static_assertions::assert_impl_all; + + #[test] + fn should_reflect_hashmaps() { + assert_impl_all!(std::collections::HashMap: Reflect); + } +} diff --git a/crates/bevy_reflect/src/impls/std/collections/hash_set.rs b/crates/bevy_reflect/src/impls/std/collections/hash_set.rs new file mode 100644 index 0000000000..26d2dd47ca --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/collections/hash_set.rs @@ -0,0 +1,17 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashset; +#[cfg(feature = "functions")] +use crate::{from_reflect::FromReflect, type_path::TypePath, type_registry::GetTypeRegistration}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashset!(::std::collections::HashSet); +impl_type_path!(::std::collections::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::std::collections::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); diff --git a/crates/bevy_reflect/src/impls/std/collections/mod.rs b/crates/bevy_reflect/src/impls/std/collections/mod.rs new file mode 100644 index 0000000000..2bde6a0653 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/collections/mod.rs @@ -0,0 +1,2 @@ +mod hash_map; +mod hash_set; diff --git a/crates/bevy_reflect/src/impls/std/ffi.rs b/crates/bevy_reflect/src/impls/std/ffi.rs new file mode 100644 index 0000000000..92cacc3b66 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/ffi.rs @@ -0,0 +1,18 @@ +#[cfg(any(unix, windows))] +use crate::type_registry::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect_derive::impl_reflect_opaque; + +// `Serialize` and `Deserialize` only for platforms supported by serde: +// https://github.com/serde-rs/serde/blob/3ffb86fc70efd3d329519e2dddfa306cc04f167c/serde/src/de/impls.rs#L1732 +#[cfg(any(unix, windows))] +impl_reflect_opaque!(::std::ffi::OsString( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); + +#[cfg(not(any(unix, windows)))] +impl_reflect_opaque!(::std::ffi::OsString(Clone, Debug, Hash, PartialEq)); diff --git a/crates/bevy_reflect/src/impls/std/mod.rs b/crates/bevy_reflect/src/impls/std/mod.rs new file mode 100644 index 0000000000..b3586e4d7d --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/mod.rs @@ -0,0 +1,46 @@ +mod collections; +mod ffi; +mod path; + +#[cfg(test)] +mod tests { + use crate::{FromReflect, PartialReflect}; + use std::collections::HashMap; + use std::path::Path; + + #[test] + fn should_partial_eq_hash_map() { + let mut a = >::default(); + a.insert(0usize, 1.23_f64); + let b = a.clone(); + let mut c = >::default(); + c.insert(0usize, 3.21_f64); + + let a: &dyn PartialReflect = &a; + let b: &dyn PartialReflect = &b; + let c: &dyn PartialReflect = &c; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn path_should_from_reflect() { + let path = Path::new("hello_world.rs"); + let output = <&'static Path as FromReflect>::from_reflect(&path).unwrap(); + assert_eq!(path, output); + } + + #[test] + fn type_id_should_from_reflect() { + let type_id = core::any::TypeId::of::(); + let output = ::from_reflect(&type_id).unwrap(); + assert_eq!(type_id, output); + } + + #[test] + fn static_str_should_from_reflect() { + let expected = "Hello, World!"; + let output = <&'static str as FromReflect>::from_reflect(&expected).unwrap(); + assert_eq!(expected, output); + } +} diff --git a/crates/bevy_reflect/src/impls/std/path.rs b/crates/bevy_reflect/src/impls/std/path.rs new file mode 100644 index 0000000000..a73ee44141 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/path.rs @@ -0,0 +1,306 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::ApplyError, + type_info::{OpaqueInfo, TypeInfo, Typed}, + type_path::DynamicTypePath, + type_registry::{ + FromType, GetTypeRegistration, ReflectDeserialize, ReflectFromPtr, ReflectSerialize, + TypeRegistration, + }, + utility::{reflect_hasher, NonGenericTypeInfoCell}, +}; +use alloc::borrow::Cow; +use bevy_platform::prelude::*; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; +use core::any::Any; +use core::fmt; +use core::hash::{Hash, Hasher}; +use std::path::Path; + +impl_reflect_opaque!(::std::path::PathBuf( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); + +impl PartialReflect for &'static Path { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(*self)) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for &'static Path { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for &'static Path { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for &'static Path { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for &'static Path { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static Path); + +impl PartialReflect for Cow<'static, Path> { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(self.clone())) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self, f) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for Cow<'static, Path> { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for Cow<'static, Path> { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl_type_path!(::std::path::Path); + +impl FromReflect for Cow<'static, Path> { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::()?.clone()) + } +} + +impl GetTypeRegistration for Cow<'static, Path> { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, Path>); diff --git a/crates/bevy_reflect/src/kind.rs b/crates/bevy_reflect/src/kind.rs index 3eef10d0e5..e8a2310b0f 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 reference 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 reference 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 58e9b8714f..99a0a1f7b5 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( @@ -8,8 +7,8 @@ )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Reflection in Rust. @@ -521,7 +520,7 @@ //! and displaying it in error messages. //! //! [Reflection]: https://en.wikipedia.org/wiki/Reflective_programming -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ //! [limitations]: #limitations //! [`bevy_reflect`]: crate //! [introspection]: https://en.wikipedia.org/wiki/Type_introspection @@ -589,7 +588,14 @@ mod type_path; mod type_registry; mod impls { + mod alloc; + mod bevy_platform; + mod core; mod foldhash; + #[cfg(feature = "hashbrown")] + mod hashbrown; + mod macros; + #[cfg(feature = "std")] mod std; #[cfg(feature = "glam")] @@ -995,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: /// - @@ -1577,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); @@ -2601,7 +2606,7 @@ bevy_reflect::tests::Test { let foo = Foo { a: 1 }; let foo: &dyn Reflect = &foo; - assert_eq!("123", format!("{:?}", foo)); + assert_eq!("123", format!("{foo:?}")); } #[test] @@ -2610,9 +2615,11 @@ bevy_reflect::tests::Test { #[reflect(where T: Default)] struct Foo(String, #[reflect(ignore)] PhantomData); + #[expect(dead_code, reason = "Bar is never constructed")] #[derive(Default, TypePath)] struct Bar; + #[expect(dead_code, reason = "Baz is never constructed")] #[derive(TypePath)] struct Baz; @@ -2626,6 +2633,7 @@ bevy_reflect::tests::Test { #[reflect(where)] struct Foo(String, #[reflect(ignore)] PhantomData); + #[expect(dead_code, reason = "Bar is never constructed")] #[derive(TypePath)] struct Bar; @@ -2660,6 +2668,7 @@ bevy_reflect::tests::Test { #[reflect(where T::Assoc: core::fmt::Display)] struct Foo(T::Assoc); + #[expect(dead_code, reason = "Bar is never constructed")] #[derive(TypePath)] struct Bar; @@ -2667,6 +2676,7 @@ bevy_reflect::tests::Test { type Assoc = usize; } + #[expect(dead_code, reason = "Baz is never constructed")] #[derive(TypePath)] struct Baz; @@ -2854,7 +2864,7 @@ bevy_reflect::tests::Test { test_unknown_tuple_struct.insert(14); test_struct.insert("unknown_tuplestruct", test_unknown_tuple_struct); assert_eq!( - format!("{:?}", test_struct), + format!("{test_struct:?}"), "DynamicStruct(bevy_reflect::tests::TestStruct { \ tuple: DynamicTuple((0, 1)), \ tuple_struct: DynamicTupleStruct(bevy_reflect::tests::TestTupleStruct(8)), \ diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index 7e768b8f1b..4ecdb63275 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -191,8 +191,7 @@ impl DynamicList { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::List(_)), - "expected TypeInfo::List but received: {:?}", - represented_type + "expected TypeInfo::List but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index e96537e67d..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 { @@ -236,8 +233,7 @@ impl DynamicMap { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Map(_)), - "expected TypeInfo::Map but received: {:?}", - represented_type + "expected TypeInfo::Map but received: {represented_type:?}" ); } @@ -253,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") } } @@ -267,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( @@ -321,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 } } @@ -342,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, @@ -450,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, @@ -503,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: @@ -583,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 /// @@ -598,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 /// @@ -614,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() { @@ -742,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 2adfb6db6c..ffe9be54fe 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, }, } @@ -135,34 +150,37 @@ where /// Applies a reflected value to this value. /// - /// If a type implements an [introspection subtrait], then the semantics of this + /// If `Self` implements a [reflection subtrait], then the semantics of this /// method are as follows: - /// - If `T` is a [`Struct`], then the value of each named field of `value` is + /// - If `Self` is a [`Struct`], then the value of each named field of `value` is /// applied to the corresponding named field of `self`. Fields which are /// not present in both structs are ignored. - /// - If `T` is a [`TupleStruct`] or [`Tuple`], then the value of each + /// - If `Self` is a [`TupleStruct`] or [`Tuple`], then the value of each /// numbered field is applied to the corresponding numbered field of /// `self.` Fields which are not present in both values are ignored. - /// - If `T` is an [`Enum`], then the variant of `self` is `updated` to match + /// - If `Self` is an [`Enum`], then the variant of `self` is `updated` to match /// the variant of `value`. The corresponding fields of that variant are /// applied from `value` onto `self`. Fields which are not present in both /// values are ignored. - /// - If `T` is a [`List`] or [`Array`], then each element of `value` is applied + /// - If `Self` is a [`List`] or [`Array`], then each element of `value` is applied /// to the corresponding element of `self`. Up to `self.len()` items are applied, /// and excess elements in `value` are appended to `self`. - /// - If `T` is a [`Map`], then for each key in `value`, the associated + /// - 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. - /// - If `T` is none of these, then `value` is downcast to `T`, cloned, and + /// 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. 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`. /// - /// Note that `Reflect` must be implemented manually for [`List`]s and - /// [`Map`]s in order to achieve the correct semantics, as derived + /// Note that `Reflect` must be implemented manually for [`List`]s, + /// [`Map`]s, and [`Set`]s in order to achieve the correct semantics, as derived /// implementations will have the semantics for [`Struct`], [`TupleStruct`], [`Enum`] - /// or none of the above depending on the kind of type. For lists and maps, use the - /// [`list_apply`] and [`map_apply`] helper functions when implementing this method. + /// or none of the above depending on the kind of type. For lists, maps, and sets, use the + /// [`list_apply`], [`map_apply`], and [`set_apply`] helper functions when implementing this method. /// - /// [introspection subtrait]: crate#the-introspection-subtraits + /// [reflection subtrait]: crate#the-reflection-subtraits /// [`Struct`]: crate::Struct /// [`TupleStruct`]: crate::TupleStruct /// [`Tuple`]: crate::Tuple @@ -170,17 +188,19 @@ where /// [`List`]: crate::List /// [`Array`]: crate::Array /// [`Map`]: crate::Map + /// [`Set`]: crate::Set /// [`list_apply`]: crate::list_apply /// [`map_apply`]: crate::map_apply + /// [`set_apply`]: crate::set_apply /// /// # Panics /// /// Derived implementations of this method will panic: - /// - If the type of `value` is not of the same kind as `T` (e.g. if `T` is + /// - If the type of `value` is not of the same kind as `Self` (e.g. if `Self` is /// a `List`, while `value` is a `Struct`). - /// - If `T` is any complex type and the corresponding fields or elements of + /// - If `Self` is any complex type and the corresponding fields or elements of /// `self` and `value` are not of the same type. - /// - If `T` is an opaque type and `self` cannot be downcast to `T` + /// - If `Self` is an opaque type and `value` cannot be downcast to `Self` fn apply(&mut self, value: &dyn PartialReflect) { PartialReflect::try_apply(self, value).unwrap(); } @@ -293,6 +313,24 @@ where }) } + /// For a type implementing [`PartialReflect`], combines `reflect_clone` and + /// `take` in a useful fashion, automatically constructing an appropriate + /// [`ReflectCloneError`] if the downcast fails. + /// + /// This is an associated function, rather than a method, because methods + /// with generic types prevent dyn-compatibility. + fn reflect_clone_and_take(&self) -> Result + where + Self: TypePath + Sized, + { + self.reflect_clone()? + .take() + .map_err(|_| ReflectCloneError::FailedDowncast { + expected: Cow::Borrowed(::type_path()), + received: Cow::Owned(self.reflect_type_path().to_string()), + }) + } + /// Returns a hash of the value (which includes the type). /// /// If the underlying type does not support hashing, returns `None`. @@ -570,7 +608,7 @@ impl TypePath for dyn Reflect { macro_rules! impl_full_reflect { ($(<$($id:ident),* $(,)?>)? for $ty:ty $(where $($tt:tt)*)?) => { impl $(<$($id),*>)? $crate::Reflect for $ty $(where $($tt)*)? { - fn into_any(self: Box) -> Box { + fn into_any(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { self } @@ -582,7 +620,7 @@ macro_rules! impl_full_reflect { self } - fn into_reflect(self: Box) -> Box { + fn into_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { self } @@ -596,8 +634,8 @@ macro_rules! impl_full_reflect { fn set( &mut self, - value: Box, - ) -> Result<(), Box> { + value: bevy_platform::prelude::Box, + ) -> Result<(), bevy_platform::prelude::Box> { *self = ::take(value)?; Ok(()) } 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/error_utils.rs b/crates/bevy_reflect/src/serde/de/error_utils.rs index d570c47f0c..58adcfe920 100644 --- a/crates/bevy_reflect/src/serde/de/error_utils.rs +++ b/crates/bevy_reflect/src/serde/de/error_utils.rs @@ -23,7 +23,7 @@ thread_local! { pub(super) fn make_custom_error(msg: impl Display) -> E { #[cfg(feature = "debug_stack")] return TYPE_INFO_STACK - .with_borrow(|stack| E::custom(format_args!("{} (stack: {:?})", msg, stack))); + .with_borrow(|stack| E::custom(format_args!("{msg} (stack: {stack:?})"))); #[cfg(not(feature = "debug_stack"))] return E::custom(msg); } 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 a2c3fe63ed..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; @@ -400,7 +402,7 @@ mod tests { }; // Poor man's comparison since we can't derive PartialEq for Arc - assert_eq!(format!("{:?}", expected), format!("{:?}", output)); + assert_eq!(format!("{expected:?}"), format!("{output:?}",)); let unexpected = Level { name: String::from("Level 1"), @@ -414,7 +416,7 @@ mod tests { }; // Poor man's comparison since we can't derive PartialEq for Arc - assert_ne!(format!("{:?}", unexpected), format!("{:?}", output)); + assert_ne!(format!("{unexpected:?}"), format!("{output:?}")); } #[test] diff --git a/crates/bevy_reflect/src/serde/ser/error_utils.rs b/crates/bevy_reflect/src/serde/ser/error_utils.rs index d252e7f591..8f38a0742a 100644 --- a/crates/bevy_reflect/src/serde/ser/error_utils.rs +++ b/crates/bevy_reflect/src/serde/ser/error_utils.rs @@ -23,7 +23,7 @@ thread_local! { pub(super) fn make_custom_error(msg: impl Display) -> E { #[cfg(feature = "debug_stack")] return TYPE_INFO_STACK - .with_borrow(|stack| E::custom(format_args!("{} (stack: {:?})", msg, stack))); + .with_borrow(|stack| E::custom(format_args!("{msg} (stack: {stack:?})"))); #[cfg(not(feature = "debug_stack"))] return E::custom(msg); } 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 b1b9147e4e..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>, @@ -158,8 +163,7 @@ impl DynamicSet { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Set(_)), - "expected TypeInfo::Set but received: {:?}", - represented_type + "expected TypeInfo::Set but received: {represented_type:?}" ); } @@ -206,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), @@ -442,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 /// @@ -477,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}; @@ -506,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 b6284a8d79..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, @@ -292,8 +294,7 @@ impl DynamicStruct { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Struct(_)), - "expected TypeInfo::Struct but received: {:?}", - represented_type + "expected TypeInfo::Struct but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 9f81d274ae..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, @@ -227,8 +228,7 @@ impl DynamicTuple { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Tuple(_)), - "expected TypeInfo::Tuple but received: {:?}", - represented_type + "expected TypeInfo::Tuple but received: {represented_type:?}" ); } self.represented_type = represented_type; @@ -649,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 410a794f68..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, @@ -242,8 +243,7 @@ impl DynamicTupleStruct { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::TupleStruct(_)), - "expected TypeInfo::TupleStruct but received: {:?}", - represented_type + "expected TypeInfo::TupleStruct but received: {represented_type:?}" ); } 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 d2e3395f77..e7a40c65ba 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -1,39 +1,44 @@ [package] name = "bevy_remote" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "The Bevy Remote Protocol" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" 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" hyper = { version = "1", features = ["server", "http1"] } serde = { version = "1", features = ["derive"] } -serde_json = { version = "1" } +serde_json = "1.0.140" http-body-util = "0.1" async-channel = "2" +bevy_log = { version = "0.17.0-dev", path = "../bevy_log" } # dependencies that will not compile on wasm [target.'cfg(not(target_family = "wasm"))'.dependencies] diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index b59f08604b..e847da08ed 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -8,12 +8,13 @@ use bevy_ecs::{ entity::Entity, event::EventCursor, hierarchy::ChildOf, + lifecycle::RemovedComponentEntity, query::QueryBuilder, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, - removal_detection::RemovedComponentEntity, system::{In, Local}, world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, }; +use bevy_log::warn_once; use bevy_platform::collections::HashMap; use bevy_reflect::{ serde::{ReflectSerializer, TypedReflectDeserializer}, @@ -24,7 +25,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, }; @@ -310,7 +314,7 @@ pub struct BrpQuery { /// /// [full path]: bevy_reflect::TypePath::type_path #[serde(default)] - pub option: Vec, + pub option: ComponentSelector, /// The [full path] of the type name of each component that is to be checked /// for presence. @@ -522,7 +526,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, }); }; @@ -570,7 +574,8 @@ pub fn process_remote_get_watching_request( ); continue; }; - let Some(component_id) = world.components().get_id(type_registration.type_id()) else { + let Some(component_id) = world.components().get_valid_id(type_registration.type_id()) + else { let err = BrpError::component_error(format!("Unknown component: `{component_path}`")); if strict { return Err(err); @@ -695,7 +700,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, }); }; @@ -703,6 +708,32 @@ fn reflect_component( Ok(serialized_object) } +/// A selector for components in a query. +/// +/// This can either be a list of component paths or an "all" selector that +/// indicates that all components should be selected. +/// The "all" selector is useful when you want to retrieve all components +/// present on an entity without specifying each one individually. +/// The paths in the `Paths` variant must be the [full type paths]: e.g. +/// `bevy_transform::components::transform::Transform`, not just +/// `Transform`. +/// +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ComponentSelector { + /// An "all" selector that indicates all components should be selected. + All, + /// A list of component paths to select as optional components. + #[serde(untagged)] + Paths(Vec), +} + +impl Default for ComponentSelector { + fn default() -> Self { + Self::Paths(Vec::default()) + } +} + /// Handles a `bevy/query` request coming from a client. pub fn process_remote_query_request(In(params): In>, world: &mut World) -> BrpResult { let BrpQueryParams { @@ -711,34 +742,69 @@ pub fn process_remote_query_request(In(params): In>, world: &mut W option, has, }, - filter: BrpQueryFilter { without, with }, + filter, strict, - } = parse_some(params)?; + } = match params { + Some(params) => parse_some(Some(params))?, + None => BrpQueryParams { + data: BrpQuery { + components: Vec::new(), + option: ComponentSelector::default(), + has: Vec::new(), + }, + filter: BrpQueryFilter::default(), + strict: false, + }, + }; let app_type_registry = world.resource::().clone(); let type_registry = app_type_registry.read(); - let components = get_component_ids(&type_registry, world, components, strict) - .map_err(BrpError::component_error)?; - let option = get_component_ids(&type_registry, world, option, strict) - .map_err(BrpError::component_error)?; - let has = + // Required components: must be present + let (required, unregistered_in_required) = + get_component_ids(&type_registry, world, components.clone(), strict) + .map_err(BrpError::component_error)?; + + // Optional components: Option<&T> or all reflectable if "all" + let (optional, _) = match &option { + ComponentSelector::Paths(paths) => { + get_component_ids(&type_registry, world, paths.clone(), strict) + .map_err(BrpError::component_error)? + } + ComponentSelector::All => (Vec::new(), Vec::new()), + }; + + // Has components: presence check + let (has_ids, unregistered_in_has) = get_component_ids(&type_registry, world, has, strict).map_err(BrpError::component_error)?; - let without = get_component_ids(&type_registry, world, without, strict) - .map_err(BrpError::component_error)?; - let with = get_component_ids(&type_registry, world, with, strict) + + // Filters + let (without, _) = get_component_ids(&type_registry, world, filter.without.clone(), strict) .map_err(BrpError::component_error)?; + let (with, unregistered_in_with) = + get_component_ids(&type_registry, world, filter.with.clone(), strict) + .map_err(BrpError::component_error)?; + + // When "strict" is false: + // - Unregistered components in "option" and "without" are ignored. + // - Unregistered components in "has" are considered absent from the entity. + // - Unregistered components in "components" and "with" result in an empty + // response since they specify hard requirements. + // If strict, fail if any required or with components are unregistered + if !unregistered_in_required.is_empty() || !unregistered_in_with.is_empty() { + return serde_json::to_value(BrpQueryResponse::default()).map_err(BrpError::internal); + } let mut query = QueryBuilder::::new(world); - for (_, component) in &components { + for (_, component) in &required { query.ref_id(*component); } - for (_, option) in &option { + for (_, option) in &optional { query.optional(|query| { query.ref_id(*option); }); } - for (_, has) in &has { + for (_, has) in &has_ids { query.optional(|query| { query.ref_id(*has); }); @@ -750,51 +816,124 @@ pub fn process_remote_query_request(In(params): In>, world: &mut W query.with_id(with); } - // At this point, we can safely unify `components` and `option`, since we only retrieved - // entities that actually have all the `components` already. - // - // We also will just collect the `ReflectComponent` values from the type registry all - // at once so that we can reuse them between components. - let paths_and_reflect_components: Vec<(&str, &ReflectComponent)> = components - .into_iter() - .chain(option) - .map(|(type_id, _)| reflect_component_from_id(type_id, &type_registry)) - .collect::>>() - .map_err(BrpError::component_error)?; - - // ... and the analogous construction for `has`: - let has_paths_and_reflect_components: Vec<(&str, &ReflectComponent)> = has - .into_iter() - .map(|(type_id, _)| reflect_component_from_id(type_id, &type_registry)) + // Prepare has reflect info + let has_paths_and_reflect_components: Vec<(&str, &ReflectComponent)> = has_ids + .iter() + .map(|(type_id, _)| reflect_component_from_id(*type_id, &type_registry)) .collect::>>() .map_err(BrpError::component_error)?; let mut response = BrpQueryResponse::default(); let mut query = query.build(); + for row in query.iter(world) { - // The map of component values: - let components_map = build_components_map( - row.clone(), - paths_and_reflect_components.iter().copied(), + let entity_id = row.id(); + let entity_ref = world.get_entity(entity_id).expect("Entity should exist"); + + // Required components + let mut components_map = serialize_components( + entity_ref, &type_registry, - ) - .map_err(BrpError::component_error)?; + required + .iter() + .map(|(type_id, component_id)| (*type_id, Some(*component_id))), + ); + + // Optional components + match &option { + ComponentSelector::All => { + // Add all reflectable components present on the entity (as Option<&T>) + let all_optionals = + entity_ref + .archetype() + .components() + .filter_map(|component_id| { + let info = world.components().get_info(component_id)?; + let type_id = info.type_id()?; + // Skip required components (already included) + if required.iter().any(|(_, cid)| cid == &component_id) { + return None; + } + Some((type_id, Some(component_id))) + }); + components_map.extend(serialize_components( + entity_ref, + &type_registry, + all_optionals, + )); + } + ComponentSelector::Paths(_) => { + // Add only the requested optional components (as Option<&T>) + let optionals = optional.iter().filter(|(_, component_id)| { + // Skip required components (already included) + !required.iter().any(|(_, cid)| cid == component_id) + }); + components_map.extend(serialize_components( + entity_ref, + &type_registry, + optionals + .clone() + .map(|(type_id, component_id)| (*type_id, Some(*component_id))), + )); + } + } // The map of boolean-valued component presences: let has_map = build_has_map( row.clone(), has_paths_and_reflect_components.iter().copied(), + &unregistered_in_has, ); - response.push(BrpQueryRow { + + let query_row = BrpQueryRow { entity: row.id(), components: components_map, has: has_map, - }); + }; + + response.push(query_row); } serde_json::to_value(response).map_err(BrpError::internal) } +/// Serializes the specified components for an entity. +/// The iterator yields ([`TypeId`], Option<[`ComponentId`]>). +fn serialize_components( + entity_ref: EntityRef, + type_registry: &TypeRegistry, + components: impl Iterator)>, +) -> HashMap { + let mut components_map = HashMap::new(); + for (type_id, component_id_opt) in components { + let Some(type_registration) = type_registry.get(type_id) else { + continue; + }; + if let Some(reflect_component) = type_registration.data::() { + // If a component_id is provided, check if the entity has it + if let Some(component_id) = component_id_opt { + if !entity_ref.contains_id(component_id) { + continue; + } + } + if let Some(reflected) = reflect_component.reflect(entity_ref) { + let reflect_serializer = + ReflectSerializer::new(reflected.as_partial_reflect(), type_registry); + if let Ok(Value::Object(obj)) = serde_json::to_value(&reflect_serializer) { + components_map.extend(obj); + } else { + warn_once!( + "Failed to serialize component `{}` for entity {:?}", + type_registration.type_info().type_path(), + entity_ref.id() + ); + } + } + } + } + components_map +} + /// Handles a `bevy/spawn` request coming from a client. pub fn process_remote_spawn_request(In(params): In>, world: &mut World) -> BrpResult { let BrpSpawnParams { components } = parse_some(params)?; @@ -1024,12 +1163,19 @@ pub fn process_remote_remove_request( let type_registry = app_type_registry.read(); let component_ids = get_component_ids(&type_registry, world, components, true) + .and_then(|(registered, unregistered)| { + if unregistered.is_empty() { + Ok(registered) + } else { + Err(anyhow!("Unregistered component types: {:?}", unregistered)) + } + }) .map_err(BrpError::component_error)?; // Remove the components. let mut entity_world_mut = get_entity_mut(world, entity)?; - for (_, component_id) in component_ids { - entity_world_mut.remove_by_id(component_id); + for (_, component_id) in component_ids.iter() { + entity_world_mut.remove_by_id(*component_id); } Ok(Value::Null) @@ -1111,7 +1257,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. @@ -1170,7 +1316,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()); } } @@ -1183,7 +1329,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()); } } } @@ -1204,24 +1350,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 @@ -1229,7 +1378,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 @@ -1238,10 +1387,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::>(); @@ -1264,8 +1412,9 @@ fn get_entity_mut(world: &mut World, entity: Entity) -> Result, strict: bool, -) -> AnyhowResult> { +) -> AnyhowResult<(Vec<(TypeId, ComponentId)>, Vec)> { let mut component_ids = vec![]; + let mut unregistered_components = vec![]; for component_path in component_paths { - let type_id = get_component_type_registration(type_registry, &component_path)?.type_id(); - let Some(component_id) = world.components().get_id(type_id) else { - if strict { - return Err(anyhow!( - "Component `{}` isn't used in the world", - component_path - )); - } - continue; - }; - - component_ids.push((type_id, component_id)); + let maybe_component_tuple = get_component_type_registration(type_registry, &component_path) + .ok() + .and_then(|type_registration| { + let type_id = type_registration.type_id(); + world + .components() + .get_valid_id(type_id) + .map(|component_id| (type_id, component_id)) + }); + if let Some((type_id, component_id)) = maybe_component_tuple { + component_ids.push((type_id, component_id)); + } else if strict { + return Err(anyhow!( + "Component `{}` isn't registered or used in the world", + component_path + )); + } else { + unregistered_components.push(component_path); + } } - Ok(component_ids) + Ok((component_ids, unregistered_components)) } -/// Given an entity (`entity_ref`) and a list of reflected component information -/// (`paths_and_reflect_components`), return a map which associates each component to -/// its serialized value from the entity. -/// -/// This is intended to be used on an entity which has already been filtered; components -/// where the value is not present on an entity are simply skipped. -fn build_components_map<'a>( - entity_ref: FilteredEntityRef, - paths_and_reflect_components: impl Iterator, - type_registry: &TypeRegistry, -) -> AnyhowResult> { - let mut serialized_components_map = >::default(); - - for (type_path, reflect_component) in paths_and_reflect_components { - let Some(reflected) = reflect_component.reflect(entity_ref.clone()) else { - continue; - }; - - let reflect_serializer = - ReflectSerializer::new(reflected.as_partial_reflect(), type_registry); - let Value::Object(serialized_object) = serde_json::to_value(&reflect_serializer)? else { - return Err(anyhow!("Component `{}` could not be serialized", type_path)); - }; - - serialized_components_map.extend(serialized_object.into_iter()); - } - - Ok(serialized_components_map) -} - -/// Given an entity (`entity_ref`) and list of reflected component information -/// (`paths_and_reflect_components`), return a map which associates each component to -/// a boolean value indicating whether or not that component is present on the entity. +/// Given an entity (`entity_ref`), +/// a list of reflected component information (`paths_and_reflect_components`) +/// and a list of unregistered components, +/// return a map which associates each component to a boolean value indicating +/// whether or not that component is present on the entity. +/// Unregistered components are considered absent from the entity. fn build_has_map<'a>( entity_ref: FilteredEntityRef, paths_and_reflect_components: impl Iterator, + unregistered_components: &[String], ) -> HashMap { let mut has_map = >::default(); @@ -1338,6 +1469,9 @@ fn build_has_map<'a>( let has = reflect_component.contains(entity_ref.clone()); has_map.insert(type_path.to_owned(), Value::Bool(has)); } + unregistered_components.iter().for_each(|component| { + has_map.insert(component.to_owned(), Value::Bool(false)); + }); has_map } diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 97b2e453e7..25c1835faf 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -136,6 +136,7 @@ //! - `components` (optional): An array of [fully-qualified type names] of components to fetch, //! see _below_ example for a query to list all the type names in **your** project. //! - `option` (optional): An array of fully-qualified type names of components to fetch optionally. +//! to fetch all reflectable components, you can pass in the string `"all"`. //! - `has` (optional): An array of fully-qualified type names of components whose presence will be //! reported as boolean values. //! - `filter` (optional): @@ -153,7 +154,141 @@ //! - `has`: A map associating each type name from `has` to a boolean value indicating whether or not the //! entity has that component. If `has` was empty or omitted, this key will be omitted in the response. //! +//! ### Example +//! To use the query API and retrieve Transform data for all entities that have a Transform +//! use this query: //! +//! ```json +//! { +//! "jsonrpc": "2.0", +//! "method": "bevy/query", +//! "id": 0, +//! "params": { +//! "data": { +//! "components": ["bevy_transform::components::transform::Transform"] +//! "option": [], +//! "has": [] +//! }, +//! "filter": { +//! "with": [], +//! "without": [] +//! }, +//! "strict": false +//! } +//! } +//! ``` +//! +//! +//! To query all entities and all of their Reflectable components (and retrieve their values), you can pass in "all" for the option field: +//! ```json +//! { +//! "jsonrpc": "2.0", +//! "method": "bevy/query", +//! "id": 0, +//! "params": { +//! "data": { +//! "components": [] +//! "option": "all", +//! "has": [] +//! }, +//! "filter": { +//! "with": [], +//! "without": [] +//! }, +//! "strict": false +//! } +//! } +//! ``` +//! +//! This should return you something like the below (in a larger list): +//! ```json +//! { +//! "components": { +//! "bevy_core_pipeline::core_3d::camera_3d::Camera3d": { +//! "depth_load_op": { +//! "Clear": 0.0 +//! }, +//! "depth_texture_usages": 16, +//! "screen_space_specular_transmission_quality": "Medium", +//! "screen_space_specular_transmission_steps": 1 +//! }, +//! "bevy_core_pipeline::tonemapping::DebandDither": "Enabled", +//! "bevy_core_pipeline::tonemapping::Tonemapping": "TonyMcMapface", +//! "bevy_pbr::cluster::ClusterConfig": { +//! "FixedZ": { +//! "dynamic_resizing": true, +//! "total": 4096, +//! "z_config": { +//! "far_z_mode": "MaxClusterableObjectRange", +//! "first_slice_depth": 5.0 +//! }, +//! "z_slices": 24 +//! } +//! }, +//! "bevy_render::camera::camera::Camera": { +//! "clear_color": "Default", +//! "is_active": true, +//! "msaa_writeback": true, +//! "order": 0, +//! "sub_camera_view": null, +//! "target": { +//! "Window": "Primary" +//! }, +//! "viewport": null +//! }, +//! "bevy_render::camera::projection::Projection": { +//! "Perspective": { +//! "aspect_ratio": 1.7777777910232544, +//! "far": 1000.0, +//! "fov": 0.7853981852531433, +//! "near": 0.10000000149011612 +//! } +//! }, +//! "bevy_render::primitives::Frustum": {}, +//! "bevy_render::sync_world::RenderEntity": 4294967291, +//! "bevy_render::sync_world::SyncToRenderWorld": {}, +//! "bevy_render::view::Msaa": "Sample4", +//! "bevy_render::view::visibility::InheritedVisibility": true, +//! "bevy_render::view::visibility::ViewVisibility": false, +//! "bevy_render::view::visibility::Visibility": "Inherited", +//! "bevy_render::view::visibility::VisibleEntities": {}, +//! "bevy_transform::components::global_transform::GlobalTransform": [ +//! 0.9635179042816162, +//! -3.725290298461914e-9, +//! 0.26764383912086487, +//! 0.11616238951683044, +//! 0.9009039402008056, +//! -0.4181846082210541, +//! -0.24112138152122495, +//! 0.4340185225009918, +//! 0.8680371046066284, +//! -2.5, +//! 4.5, +//! 9.0 +//! ], +//! "bevy_transform::components::transform::Transform": { +//! "rotation": [ +//! -0.22055435180664065, +//! -0.13167093694210052, +//! -0.03006339818239212, +//! 0.9659786224365234 +//! ], +//! "scale": [ +//! 1.0, +//! 1.0, +//! 1.0 +//! ], +//! "translation": [ +//! -2.5, +//! 4.5, +//! 9.0 +//! ] +//! }, +//! "bevy_transform::components::transform::TransformTreeChanged": null +//! }, +//! "entity": 4294967261 +//!}, +//! ``` //! //! ### `bevy/spawn` //! @@ -364,6 +499,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 +676,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 aa6b6e239c..4183ca8085 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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,30 +49,36 @@ 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" } -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_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.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } +bevy_light = { path = "../bevy_light", optional = true, version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -78,26 +87,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 = [ @@ -105,7 +115,7 @@ profiling = { version = "1", features = [ ], optional = true } async-channel = "2.3.0" nonmax = "0.5" -smallvec = { version = "1.11", features = ["const_new"] } +smallvec = { version = "1", default-features = false, features = ["const_new"] } offset-allocator = "0.2" variadics_please = "1.1" tracing = { version = "0.1", default-features = false, features = ["std"] } @@ -117,7 +127,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", default-features = false, features = [ +naga_oil = { version = "0.18", default-features = false, features = [ "test_shader", ] } @@ -125,7 +135,7 @@ naga_oil = { version = "0.17", default-features = false, features = [ proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.17" +naga_oil = { version = "0.18" } js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', @@ -138,22 +148,19 @@ 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", ] } -[target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] -send_wrapper = "0.6.0" - [lints] workspace = true diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index c3fc40b23e..016fe88765 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -1,9 +1,9 @@ [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://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -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..b426088e22 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, @@ -1061,17 +1061,21 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { render_device: &#render_path::renderer::RenderDevice, (images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, force_no_bindless: bool, - ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { + ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { #uniform_binding_type_declarations let bindings = #render_path::render_resource::BindingResources(vec![#(#binding_impls,)*]); Ok(#render_path::render_resource::UnpreparedBindGroup { bindings, - data: #get_prepared_data, }) } + #[allow(clippy::unused_unit)] + fn bind_group_data(&self) -> Self::Data { + #get_prepared_data + } + fn bind_group_layout_entries( render_device: &#render_path::renderer::RenderDevice, force_no_bindless: bool @@ -1337,7 +1341,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/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index 7a04932bcd..1c9dcebf3d 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -4,6 +4,7 @@ mod as_bind_group; mod extract_component; mod extract_resource; +mod specializer; use bevy_macro_utils::{derive_label, BevyManifest}; use proc_macro::TokenStream; @@ -14,6 +15,10 @@ pub(crate) fn bevy_render_path() -> syn::Path { BevyManifest::shared().get_path("bevy_render") } +pub(crate) fn bevy_ecs_path() -> syn::Path { + BevyManifest::shared().get_path("bevy_ecs") +} + #[proc_macro_derive(ExtractResource)] pub fn derive_extract_resource(input: TokenStream) -> TokenStream { extract_resource::derive_extract_resource(input) @@ -80,12 +85,10 @@ pub fn derive_render_label(input: TokenStream) -> TokenStream { trait_path .segments .push(format_ident!("render_graph").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path .segments .push(format_ident!("RenderLabel").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "RenderLabel", &trait_path, &dyn_eq_path) + derive_label(input, "RenderLabel", &trait_path) } /// Derive macro generating an impl of the trait `RenderSubGraph`. @@ -98,10 +101,48 @@ pub fn derive_render_sub_graph(input: TokenStream) -> TokenStream { trait_path .segments .push(format_ident!("render_graph").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path .segments .push(format_ident!("RenderSubGraph").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "RenderSubGraph", &trait_path, &dyn_eq_path) + derive_label(input, "RenderSubGraph", &trait_path) +} + +/// Derive macro generating an impl of the trait `Specializer` +/// +/// This only works for structs whose members all implement `Specializer` +#[proc_macro_derive(Specializer, attributes(specialize, key, base_descriptor))] +pub fn derive_specialize(input: TokenStream) -> TokenStream { + specializer::impl_specializer(input) +} + +/// Derive macro generating the most common impl of the trait `SpecializerKey` +#[proc_macro_derive(SpecializerKey)] +pub fn derive_specializer_key(input: TokenStream) -> TokenStream { + specializer::impl_specializer_key(input) +} + +#[proc_macro_derive(ShaderLabel)] +pub fn derive_shader_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("ShaderLabel").into()); + derive_label(input, "ShaderLabel", &trait_path) +} + +#[proc_macro_derive(DrawFunctionLabel)] +pub fn derive_draw_function_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("DrawFunctionLabel").into()); + derive_label(input, "DrawFunctionLabel", &trait_path) } diff --git a/crates/bevy_render/macros/src/specializer.rs b/crates/bevy_render/macros/src/specializer.rs new file mode 100644 index 0000000000..d755c73736 --- /dev/null +++ b/crates/bevy_render/macros/src/specializer.rs @@ -0,0 +1,475 @@ +use bevy_macro_utils::{ + fq_std::{FQDefault, FQResult}, + get_struct_fields, +}; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + punctuated::Punctuated, + spanned::Spanned, + DeriveInput, Expr, Field, Ident, Index, Member, Meta, MetaList, Pat, Path, Token, Type, + WherePredicate, +}; + +const SPECIALIZE_ATTR_IDENT: &str = "specialize"; +const SPECIALIZE_ALL_IDENT: &str = "all"; + +const KEY_ATTR_IDENT: &str = "key"; +const KEY_DEFAULT_IDENT: &str = "default"; + +const BASE_DESCRIPTOR_ATTR_IDENT: &str = "base_descriptor"; + +enum SpecializeImplTargets { + All, + Specific(Vec), +} + +impl Parse for SpecializeImplTargets { + fn parse(input: ParseStream) -> syn::Result { + let paths = input.parse_terminated(Path::parse, Token![,])?; + if paths + .first() + .is_some_and(|p| p.is_ident(SPECIALIZE_ALL_IDENT)) + { + Ok(SpecializeImplTargets::All) + } else { + Ok(SpecializeImplTargets::Specific(paths.into_iter().collect())) + } + } +} + +#[derive(Clone)] +enum Key { + Whole, + Default, + Index(Index), + Custom(Expr), +} + +impl Key { + fn expr(&self) -> Expr { + match self { + Key::Whole => parse_quote!(key), + Key::Default => parse_quote!(#FQDefault::default()), + Key::Index(index) => { + let member = Member::Unnamed(index.clone()); + parse_quote!(key.#member) + } + Key::Custom(expr) => expr.clone(), + } + } +} + +const KEY_ERROR_MSG: &str = "Invalid key override. Must be either `default` or a valid Rust expression of the correct key type"; + +impl Parse for Key { + fn parse(input: ParseStream) -> syn::Result { + if let Ok(ident) = input.parse::() { + if ident == KEY_DEFAULT_IDENT { + Ok(Key::Default) + } else { + Err(syn::Error::new_spanned(ident, KEY_ERROR_MSG)) + } + } else { + input.parse::().map(Key::Custom).map_err(|mut err| { + err.extend(syn::Error::new(err.span(), KEY_ERROR_MSG)); + err + }) + } + } +} + +#[derive(Clone)] +struct FieldInfo { + ty: Type, + member: Member, + key: Key, + use_base_descriptor: bool, +} + +impl FieldInfo { + fn key_ty(&self, specialize_path: &Path, target_path: &Path) -> Option { + let ty = &self.ty; + matches!(self.key, Key::Whole | Key::Index(_)) + .then_some(parse_quote!(<#ty as #specialize_path::Specializer<#target_path>>::Key)) + } + + fn key_ident(&self, ident: Ident) -> Option { + matches!(self.key, Key::Whole | Key::Index(_)).then_some(ident) + } + + fn specialize_expr(&self, specialize_path: &Path, target_path: &Path) -> Expr { + let FieldInfo { + ty, member, key, .. + } = &self; + let key_expr = key.expr(); + parse_quote!(<#ty as #specialize_path::Specializer<#target_path>>::specialize(&self.#member, #key_expr, descriptor)) + } + + fn specialize_predicate(&self, specialize_path: &Path, target_path: &Path) -> WherePredicate { + let ty = &self.ty; + if matches!(&self.key, Key::Default) { + parse_quote!(#ty: #specialize_path::Specializer<#target_path, Key: #FQDefault>) + } else { + parse_quote!(#ty: #specialize_path::Specializer<#target_path>) + } + } + + fn get_base_descriptor_predicate( + &self, + specialize_path: &Path, + target_path: &Path, + ) -> WherePredicate { + let ty = &self.ty; + parse_quote!(#ty: #specialize_path::GetBaseDescriptor<#target_path>) + } +} + +fn get_field_info( + fields: &Punctuated, + targets: &SpecializeImplTargets, +) -> syn::Result> { + let mut field_info: Vec = Vec::new(); + let mut used_count = 0; + let mut single_index = 0; + for (index, field) in fields.iter().enumerate() { + let field_ty = field.ty.clone(); + let field_member = field.ident.clone().map_or( + Member::Unnamed(Index { + index: index as u32, + span: field.span(), + }), + Member::Named, + ); + let key_index = Index { + index: used_count, + span: field.span(), + }; + + let mut use_key_field = true; + let mut key = Key::Index(key_index); + let mut use_base_descriptor = false; + for attr in &field.attrs { + match &attr.meta { + Meta::Path(path) if path.is_ident(&BASE_DESCRIPTOR_ATTR_IDENT) => { + use_base_descriptor = true; + } + Meta::List(MetaList { path, tokens, .. }) if path.is_ident(&KEY_ATTR_IDENT) => { + let owned_tokens = tokens.clone().into(); + let Ok(parsed_key) = syn::parse::(owned_tokens) else { + return Err(syn::Error::new( + attr.span(), + "Invalid key override attribute", + )); + }; + key = parsed_key; + if matches!( + (&key, &targets), + (Key::Custom(_), SpecializeImplTargets::All) + ) { + return Err(syn::Error::new( + attr.span(), + "#[key(default)] is the only key override type allowed with #[specialize(all)]", + )); + } + use_key_field = false; + } + _ => {} + } + } + + if use_key_field { + used_count += 1; + single_index = index; + } + + field_info.push(FieldInfo { + ty: field_ty, + member: field_member, + key, + use_base_descriptor, + }); + } + + if used_count == 1 { + field_info[single_index].key = Key::Whole; + } + + Ok(field_info) +} + +fn get_specialize_targets( + ast: &DeriveInput, + derive_name: &str, +) -> syn::Result { + let specialize_attr = ast.attrs.iter().find_map(|attr| { + if attr.path().is_ident(SPECIALIZE_ATTR_IDENT) { + if let Meta::List(meta_list) = &attr.meta { + return Some(meta_list); + } + } + None + }); + let Some(specialize_meta_list) = specialize_attr else { + return Err(syn::Error::new( + Span::call_site(), + format!("#[derive({derive_name})] must be accompanied by #[specialize(..targets)].\n Example usages: #[specialize(RenderPipeline)], #[specialize(all)]") + )); + }; + syn::parse::(specialize_meta_list.tokens.clone().into()) +} + +macro_rules! guard { + ($expr: expr) => { + match $expr { + Ok(__val) => __val, + Err(err) => return err.to_compile_error().into(), + } + }; +} + +pub fn impl_specializer(input: TokenStream) -> TokenStream { + let bevy_render_path: Path = crate::bevy_render_path(); + let specialize_path = { + let mut path = bevy_render_path.clone(); + path.segments.push(format_ident!("render_resource").into()); + path + }; + + let ecs_path = crate::bevy_ecs_path(); + + let ast = parse_macro_input!(input as DeriveInput); + let targets = guard!(get_specialize_targets(&ast, "Specializer")); + let fields = guard!(get_struct_fields(&ast.data, "Specializer")); + let field_info = guard!(get_field_info(fields, &targets)); + + let key_idents: Vec> = field_info + .iter() + .enumerate() + .map(|(i, field_info)| field_info.key_ident(format_ident!("key{i}"))) + .collect(); + let key_tuple_idents: Vec = key_idents.iter().flatten().cloned().collect(); + let ignore_pat: Pat = parse_quote!(_); + let key_patterns: Vec = key_idents + .iter() + .map(|key_ident| match key_ident { + Some(key_ident) => parse_quote!(#key_ident), + None => ignore_pat.clone(), + }) + .collect(); + + let base_descriptor_fields = field_info + .iter() + .filter(|field| field.use_base_descriptor) + .collect::>(); + + if base_descriptor_fields.len() > 1 { + return syn::Error::new( + Span::call_site(), + "Too many #[base_descriptor] attributes found. It must be present on exactly one field", + ) + .into_compile_error() + .into(); + } + + let base_descriptor_field = base_descriptor_fields.first().copied(); + + match targets { + SpecializeImplTargets::All => { + let specialize_impl = impl_specialize_all( + &specialize_path, + &ecs_path, + &ast, + &field_info, + &key_patterns, + &key_tuple_idents, + ); + let get_base_descriptor_impl = base_descriptor_field + .map(|field_info| impl_get_base_descriptor_all(&specialize_path, &ast, field_info)) + .unwrap_or_default(); + [specialize_impl, get_base_descriptor_impl] + .into_iter() + .collect() + } + SpecializeImplTargets::Specific(targets) => { + let specialize_impls = targets.iter().map(|target| { + impl_specialize_specific( + &specialize_path, + &ecs_path, + &ast, + &field_info, + target, + &key_patterns, + &key_tuple_idents, + ) + }); + let get_base_descriptor_impls = targets.iter().filter_map(|target| { + base_descriptor_field.map(|field_info| { + impl_get_base_descriptor_specific(&specialize_path, &ast, field_info, target) + }) + }); + specialize_impls.chain(get_base_descriptor_impls).collect() + } + } +} + +fn impl_specialize_all( + specialize_path: &Path, + ecs_path: &Path, + ast: &DeriveInput, + field_info: &[FieldInfo], + key_patterns: &[Pat], + key_tuple_idents: &[Ident], +) -> TokenStream { + let target_path = Path::from(format_ident!("T")); + let key_elems: Vec = field_info + .iter() + .filter_map(|field_info| field_info.key_ty(specialize_path, &target_path)) + .collect(); + let specialize_exprs: Vec = field_info + .iter() + .map(|field_info| field_info.specialize_expr(specialize_path, &target_path)) + .collect(); + + let struct_name = &ast.ident; + let mut generics = ast.generics.clone(); + generics.params.insert( + 0, + parse_quote!(#target_path: #specialize_path::Specializable), + ); + + if !field_info.is_empty() { + let where_clause = generics.make_where_clause(); + for field in field_info { + where_clause + .predicates + .push(field.specialize_predicate(specialize_path, &target_path)); + } + } + + let (_, type_generics, _) = ast.generics.split_for_impl(); + let (impl_generics, _, where_clause) = &generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #specialize_path::Specializer<#target_path> for #struct_name #type_generics #where_clause { + type Key = (#(#key_elems),*); + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut <#target_path as #specialize_path::Specializable>::Descriptor + ) -> #FQResult<#specialize_path::Canonical, #ecs_path::error::BevyError> { + #(let #key_patterns = #specialize_exprs?;)* + #FQResult::Ok((#(#key_tuple_idents),*)) + } + } + }) +} + +fn impl_specialize_specific( + specialize_path: &Path, + ecs_path: &Path, + ast: &DeriveInput, + field_info: &[FieldInfo], + target_path: &Path, + key_patterns: &[Pat], + key_tuple_idents: &[Ident], +) -> TokenStream { + let key_elems: Vec = field_info + .iter() + .filter_map(|field_info| field_info.key_ty(specialize_path, target_path)) + .collect(); + let specialize_exprs: Vec = field_info + .iter() + .map(|field_info| field_info.specialize_expr(specialize_path, target_path)) + .collect(); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #specialize_path::Specializer<#target_path> for #struct_name #type_generics #where_clause { + type Key = (#(#key_elems),*); + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut <#target_path as #specialize_path::Specializable>::Descriptor + ) -> #FQResult<#specialize_path::Canonical, #ecs_path::error::BevyError> { + #(let #key_patterns = #specialize_exprs?;)* + #FQResult::Ok((#(#key_tuple_idents),*)) + } + } + }) +} + +fn impl_get_base_descriptor_specific( + specialize_path: &Path, + ast: &DeriveInput, + base_descriptor_field_info: &FieldInfo, + target_path: &Path, +) -> TokenStream { + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + let field_ty = &base_descriptor_field_info.ty; + let field_member = &base_descriptor_field_info.member; + TokenStream::from(quote!( + impl #impl_generics #specialize_path::GetBaseDescriptor<#target_path> for #struct_name #type_generics #where_clause { + fn get_base_descriptor(&self) -> <#target_path as #specialize_path::Specializable>::Descriptor { + <#field_ty as #specialize_path::GetBaseDescriptor<#target_path>>::get_base_descriptor(&self.#field_member) + } + } + )) +} + +fn impl_get_base_descriptor_all( + specialize_path: &Path, + ast: &DeriveInput, + base_descriptor_field_info: &FieldInfo, +) -> TokenStream { + let target_path = Path::from(format_ident!("T")); + let struct_name = &ast.ident; + let mut generics = ast.generics.clone(); + generics.params.insert( + 0, + parse_quote!(#target_path: #specialize_path::Specializable), + ); + + let where_clause = generics.make_where_clause(); + where_clause.predicates.push( + base_descriptor_field_info.get_base_descriptor_predicate(specialize_path, &target_path), + ); + + let (_, type_generics, _) = ast.generics.split_for_impl(); + let (impl_generics, _, where_clause) = &generics.split_for_impl(); + let field_ty = &base_descriptor_field_info.ty; + let field_member = &base_descriptor_field_info.member; + TokenStream::from(quote! { + impl #impl_generics #specialize_path::GetBaseDescriptor<#target_path> for #struct_name #type_generics #where_clause { + fn get_base_descriptor(&self) -> <#target_path as #specialize_path::Specializable>::Descriptor { + <#field_ty as #specialize_path::GetBaseDescriptor<#target_path>>::get_base_descriptor(&self.#field_member) + } + } + }) +} + +pub fn impl_specializer_key(input: TokenStream) -> TokenStream { + let bevy_render_path: Path = crate::bevy_render_path(); + let specialize_path = { + let mut path = bevy_render_path.clone(); + path.segments.push(format_ident!("render_resource").into()); + path + }; + + let ast = parse_macro_input!(input as DeriveInput); + let ident = ast.ident; + TokenStream::from(quote!( + impl #specialize_path::SpecializerKey for #ident { + const IS_CANONICAL: bool = true; + type Canonical = Self; + } + )) +} diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index ea5970431a..2fb0172b21 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -392,9 +392,12 @@ where } /// The buffer of GPU preprocessing work items for a single view. -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) )] pub enum PreprocessWorkItemBuffers { /// The work items we use if we aren't using indirect drawing. 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.rs b/crates/bevy_render/src/camera.rs new file mode 100644 index 0000000000..346762aecc --- /dev/null +++ b/crates/bevy_render/src/camera.rs @@ -0,0 +1,686 @@ +use crate::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + extract_resource::{ExtractResource, ExtractResourcePlugin}, + render_asset::RenderAssets, + render_graph::{CameraDriverNode, InternedRenderSubGraph, RenderGraph, RenderSubGraph}, + render_resource::TextureView, + sync_world::{RenderEntity, SyncToRenderWorld}, + texture::{GpuImage, ManualTextureViews}, + view::{ + ColorGrading, ExtractedView, ExtractedWindows, Hdr, Msaa, NoIndirectDrawing, + RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset, + }, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; + +use bevy_app::{App, Plugin, PostStartup, PostUpdate}; +use bevy_asset::{AssetEvent, AssetEventSystems, AssetId, Assets}; +pub use bevy_camera::*; +use bevy_camera::{ + primitives::Frustum, + visibility::{RenderLayers, VisibleEntities}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + change_detection::DetectChanges, + component::Component, + entity::{ContainsEntity, Entity}, + event::EventReader, + lifecycle::HookContext, + prelude::With, + query::{Has, QueryItem}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, + world::DeferredWorld, +}; +use bevy_image::Image; +use bevy_math::{vec2, Mat4, URect, UVec2, UVec4, Vec2}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_reflect::prelude::*; +use bevy_transform::components::GlobalTransform; +use bevy_window::{ + NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowResized, + WindowScaleFactorChanged, +}; +use derive_more::derive::From; +use tracing::warn; +use wgpu::TextureFormat; + +#[derive(Default)] +pub struct CameraPlugin; + +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .register_type::() + .register_type::() + .register_required_components::() + .register_required_components::() + .register_required_components::() + .register_required_components::() + .add_plugins(( + ExtractResourcePlugin::::default(), + ExtractComponentPlugin::::default(), + bevy_camera::CameraPlugin, + )) + .add_systems(PostStartup, camera_system.in_set(CameraUpdateSystems)) + .add_systems( + PostUpdate, + camera_system + .in_set(CameraUpdateSystems) + .before(AssetEventSystems) + .before(visibility::update_frusta), + ); + app.world_mut() + .register_component_hooks::() + .on_add(warn_on_no_render_graph); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .add_systems(ExtractSchedule, extract_cameras) + .add_systems(Render, sort_cameras.in_set(RenderSystems::ManageViews)); + let camera_driver_node = CameraDriverNode::new(render_app.world_mut()); + let mut render_graph = render_app.world_mut().resource_mut::(); + render_graph.add_node(crate::graph::CameraDriverLabel, camera_driver_node); + } + } +} + +fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + if !world.entity(entity).contains::() { + warn!("{}Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Consider adding a `Camera2d` or `Camera3d` component, or manually adding a `CameraRenderGraph` component if you need a custom render graph.", caller.map(|location|format!("{location}: ")).unwrap_or_default()); + } +} + +impl ExtractResource for ClearColor { + type Source = Self; + + fn extract_resource(source: &Self::Source) -> Self { + source.clone() + } +} +impl ExtractComponent for CameraMainTextureUsages { + type QueryData = &'static Self; + type QueryFilter = (); + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(*item) + } +} +impl ExtractComponent for Camera2d { + type QueryData = &'static Self; + type QueryFilter = With; + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} +impl ExtractComponent for Camera3d { + type QueryData = &'static Self; + type QueryFilter = With; + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} + +/// Configures the [`RenderGraph`] name assigned to be run for a given [`Camera`] entity. +#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)] +#[reflect(opaque)] +#[reflect(Component, Debug, Clone)] +pub struct CameraRenderGraph(InternedRenderSubGraph); + +impl CameraRenderGraph { + /// Creates a new [`CameraRenderGraph`] from any string-like type. + #[inline] + pub fn new(name: T) -> Self { + Self(name.intern()) + } + + /// Sets the graph name. + #[inline] + pub fn set(&mut self, name: T) { + self.0 = name.intern(); + } +} + +pub trait ToNormalizedRenderTarget { + /// Normalize the render target down to a more concrete value, mostly used for equality comparisons. + fn normalize(&self, primary_window: Option) -> Option; +} + +impl ToNormalizedRenderTarget for RenderTarget { + fn normalize(&self, primary_window: Option) -> Option { + match self { + RenderTarget::Window(window_ref) => window_ref + .normalize(primary_window) + .map(NormalizedRenderTarget::Window), + RenderTarget::Image(handle) => Some(NormalizedRenderTarget::Image(handle.clone())), + RenderTarget::TextureView(id) => Some(NormalizedRenderTarget::TextureView(*id)), + } + } +} + +/// Normalized version of the render target. +/// +/// Once we have this we shouldn't need to resolve it down anymore. +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord, From)] +#[reflect(Clone, PartialEq, Hash)] +pub enum NormalizedRenderTarget { + /// Window to which the camera's view is rendered. + Window(NormalizedWindowRef), + /// Image to which the camera's view is rendered. + Image(ImageRenderTarget), + /// Texture View to which the camera's view is rendered. + /// Useful when the texture view needs to be created outside of Bevy, for example OpenXR. + TextureView(ManualTextureViewHandle), +} + +impl NormalizedRenderTarget { + pub fn get_texture_view<'a>( + &self, + windows: &'a ExtractedWindows, + images: &'a RenderAssets, + manual_texture_views: &'a ManualTextureViews, + ) -> Option<&'a TextureView> { + match self { + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) + .and_then(|window| window.swap_chain_texture_view.as_ref()), + NormalizedRenderTarget::Image(image_target) => images + .get(&image_target.handle) + .map(|image| &image.texture_view), + NormalizedRenderTarget::TextureView(id) => { + manual_texture_views.get(id).map(|tex| &tex.texture_view) + } + } + } + + /// Retrieves the [`TextureFormat`] of this render target, if it exists. + pub fn get_texture_format<'a>( + &self, + windows: &'a ExtractedWindows, + images: &'a RenderAssets, + manual_texture_views: &'a ManualTextureViews, + ) -> Option { + match self { + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) + .and_then(|window| window.swap_chain_texture_format), + NormalizedRenderTarget::Image(image_target) => images + .get(&image_target.handle) + .map(|image| image.texture_format), + NormalizedRenderTarget::TextureView(id) => { + manual_texture_views.get(id).map(|tex| tex.format) + } + } + } + + pub fn get_render_target_info<'a>( + &self, + resolutions: impl IntoIterator, + images: &Assets, + manual_texture_views: &ManualTextureViews, + ) -> Option { + match self { + NormalizedRenderTarget::Window(window_ref) => resolutions + .into_iter() + .find(|(entity, _)| *entity == window_ref.entity()) + .map(|(_, window)| RenderTargetInfo { + physical_size: window.physical_size(), + scale_factor: window.resolution.scale_factor(), + }), + NormalizedRenderTarget::Image(image_target) => { + let image = images.get(&image_target.handle)?; + Some(RenderTargetInfo { + physical_size: image.size(), + scale_factor: image_target.scale_factor.0, + }) + } + NormalizedRenderTarget::TextureView(id) => { + manual_texture_views.get(id).map(|tex| RenderTargetInfo { + physical_size: tex.size, + scale_factor: 1.0, + }) + } + } + } + + // Check if this render target is contained in the given changed windows or images. + fn is_changed( + &self, + changed_window_ids: &HashSet, + changed_image_handles: &HashSet<&AssetId>, + ) -> bool { + match self { + NormalizedRenderTarget::Window(window_ref) => { + changed_window_ids.contains(&window_ref.entity()) + } + NormalizedRenderTarget::Image(image_target) => { + changed_image_handles.contains(&image_target.handle.id()) + } + NormalizedRenderTarget::TextureView(_) => true, + } + } +} + +/// System in charge of updating a [`Camera`] when its window or projection changes. +/// +/// The system detects window creation, resize, and scale factor change events to update the camera +/// [`Projection`] if needed. +/// +/// ## World Resources +/// +/// [`Res>`](Assets) -- For cameras that render to an image, this resource is used to +/// inspect information about the render target. This system will not access any other image assets. +/// +/// [`OrthographicProjection`]: crate::camera::OrthographicProjection +/// [`PerspectiveProjection`]: crate::camera::PerspectiveProjection +pub fn camera_system( + mut window_resized_events: EventReader, + mut window_created_events: EventReader, + mut window_scale_factor_changed_events: EventReader, + mut image_asset_events: EventReader>, + primary_window: Query>, + windows: Query<(Entity, &Window)>, + images: Res>, + manual_texture_views: Res, + mut cameras: Query<(&mut Camera, &mut Projection)>, +) { + let primary_window = primary_window.iter().next(); + + let mut changed_window_ids = >::default(); + changed_window_ids.extend(window_created_events.read().map(|event| event.window)); + changed_window_ids.extend(window_resized_events.read().map(|event| event.window)); + let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_events + .read() + .map(|event| event.window) + .collect(); + changed_window_ids.extend(scale_factor_changed_window_ids.clone()); + + let changed_image_handles: HashSet<&AssetId> = image_asset_events + .read() + .filter_map(|event| match event { + AssetEvent::Modified { id } | AssetEvent::Added { id } => Some(id), + _ => None, + }) + .collect(); + + for (mut camera, mut camera_projection) in &mut cameras { + let mut viewport_size = camera + .viewport + .as_ref() + .map(|viewport| viewport.physical_size); + + if let Some(normalized_target) = &camera.target.normalize(primary_window) { + if normalized_target.is_changed(&changed_window_ids, &changed_image_handles) + || camera.is_added() + || camera_projection.is_changed() + || camera.computed.old_viewport_size != viewport_size + || camera.computed.old_sub_camera_view != camera.sub_camera_view + { + let new_computed_target_info = normalized_target.get_render_target_info( + windows, + &images, + &manual_texture_views, + ); + // Check for the scale factor changing, and resize the viewport if needed. + // This can happen when the window is moved between monitors with different DPIs. + // Without this, the viewport will take a smaller portion of the window moved to + // a higher DPI monitor. + if normalized_target + .is_changed(&scale_factor_changed_window_ids, &HashSet::default()) + { + if let (Some(new_scale_factor), Some(old_scale_factor)) = ( + new_computed_target_info + .as_ref() + .map(|info| info.scale_factor), + camera + .computed + .target_info + .as_ref() + .map(|info| info.scale_factor), + ) { + let resize_factor = new_scale_factor / old_scale_factor; + if let Some(ref mut viewport) = camera.viewport { + let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2(); + viewport.physical_position = resize(viewport.physical_position); + viewport.physical_size = resize(viewport.physical_size); + viewport_size = Some(viewport.physical_size); + } + } + } + // This check is needed because when changing WindowMode to Fullscreen, the viewport may have invalid + // arguments due to a sudden change on the window size to a lower value. + // If the size of the window is lower, the viewport will match that lower value. + if let Some(viewport) = &mut camera.viewport { + let target_info = &new_computed_target_info; + if let Some(target) = target_info { + viewport.clamp_to_size(target.physical_size); + } + } + camera.computed.target_info = new_computed_target_info; + if let Some(size) = camera.logical_viewport_size() { + if size.x != 0.0 && size.y != 0.0 { + camera_projection.update(size.x, size.y); + camera.computed.clip_from_view = match &camera.sub_camera_view { + Some(sub_view) => { + camera_projection.get_clip_from_view_for_sub(sub_view) + } + None => camera_projection.get_clip_from_view(), + } + } + } + } + } + + if camera.computed.old_viewport_size != viewport_size { + camera.computed.old_viewport_size = viewport_size; + } + + if camera.computed.old_sub_camera_view != camera.sub_camera_view { + camera.computed.old_sub_camera_view = camera.sub_camera_view; + } + } +} + +#[derive(Component, Debug)] +pub struct ExtractedCamera { + pub target: Option, + pub physical_viewport_size: Option, + pub physical_target_size: Option, + pub viewport: Option, + pub render_graph: InternedRenderSubGraph, + pub order: isize, + pub output_mode: CameraOutputMode, + pub msaa_writeback: bool, + pub clear_color: ClearColorConfig, + pub sorted_camera_index_for_target: usize, + pub exposure: f32, + pub hdr: bool, +} + +pub fn extract_cameras( + mut commands: Commands, + query: Extract< + Query<( + Entity, + RenderEntity, + &Camera, + &CameraRenderGraph, + &GlobalTransform, + &VisibleEntities, + &Frustum, + Has, + Option<&ColorGrading>, + Option<&Exposure>, + Option<&TemporalJitter>, + Option<&MipBias>, + Option<&RenderLayers>, + Option<&Projection>, + Has, + )>, + >, + primary_window: Extract>>, + gpu_preprocessing_support: Res, + mapper: Extract>, +) { + let primary_window = primary_window.iter().next(); + for ( + main_entity, + render_entity, + camera, + camera_render_graph, + transform, + visible_entities, + frustum, + hdr, + color_grading, + exposure, + temporal_jitter, + mip_bias, + render_layers, + projection, + no_indirect_drawing, + ) in query.iter() + { + if !camera.is_active { + commands.entity(render_entity).remove::<( + ExtractedCamera, + ExtractedView, + RenderVisibleEntities, + TemporalJitter, + MipBias, + RenderLayers, + Projection, + NoIndirectDrawing, + ViewUniformOffset, + )>(); + continue; + } + + let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone(); + + if let ( + Some(URect { + min: viewport_origin, + .. + }), + Some(viewport_size), + Some(target_size), + ) = ( + camera.physical_viewport_rect(), + camera.physical_viewport_size(), + camera.physical_target_size(), + ) { + if target_size.x == 0 || target_size.y == 0 { + continue; + } + + let render_visible_entities = RenderVisibleEntities { + entities: visible_entities + .entities + .iter() + .map(|(type_id, entities)| { + let entities = entities + .iter() + .map(|entity| { + let render_entity = mapper + .get(*entity) + .cloned() + .map(|entity| entity.id()) + .unwrap_or(Entity::PLACEHOLDER); + (render_entity, (*entity).into()) + }) + .collect(); + (*type_id, entities) + }) + .collect(), + }; + + let mut commands = commands.entity(render_entity); + commands.insert(( + ExtractedCamera { + target: camera.target.normalize(primary_window), + viewport: camera.viewport.clone(), + physical_viewport_size: Some(viewport_size), + physical_target_size: Some(target_size), + render_graph: camera_render_graph.0, + order: camera.order, + output_mode: camera.output_mode, + msaa_writeback: camera.msaa_writeback, + clear_color: camera.clear_color, + // this will be set in sort_cameras + sorted_camera_index_for_target: 0, + exposure: exposure + .map(Exposure::exposure) + .unwrap_or_else(|| Exposure::default().exposure()), + hdr, + }, + ExtractedView { + retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0), + clip_from_view: camera.clip_from_view(), + world_from_view: *transform, + clip_from_world: None, + hdr, + viewport: UVec4::new( + viewport_origin.x, + viewport_origin.y, + viewport_size.x, + viewport_size.y, + ), + color_grading, + }, + render_visible_entities, + *frustum, + )); + + if let Some(temporal_jitter) = temporal_jitter { + commands.insert(temporal_jitter.clone()); + } else { + commands.remove::(); + } + + if let Some(mip_bias) = mip_bias { + commands.insert(mip_bias.clone()); + } else { + commands.remove::(); + } + + if let Some(render_layers) = render_layers { + commands.insert(render_layers.clone()); + } else { + commands.remove::(); + } + + if let Some(perspective) = projection { + commands.insert(perspective.clone()); + } else { + commands.remove::(); + } + + if no_indirect_drawing + || !matches!( + gpu_preprocessing_support.max_supported_mode, + GpuPreprocessingMode::Culling + ) + { + commands.insert(NoIndirectDrawing); + } else { + commands.remove::(); + } + }; + } +} + +/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system. +#[derive(Resource, Default)] +pub struct SortedCameras(pub Vec); + +pub struct SortedCamera { + pub entity: Entity, + pub order: isize, + pub target: Option, + pub hdr: bool, +} + +pub fn sort_cameras( + mut sorted_cameras: ResMut, + mut cameras: Query<(Entity, &mut ExtractedCamera)>, +) { + sorted_cameras.0.clear(); + for (entity, camera) in cameras.iter() { + sorted_cameras.0.push(SortedCamera { + entity, + order: camera.order, + target: camera.target.clone(), + hdr: camera.hdr, + }); + } + // sort by order and ensure within an order, RenderTargets of the same type are packed together + sorted_cameras + .0 + .sort_by(|c1, c2| (c1.order, &c1.target).cmp(&(c2.order, &c2.target))); + let mut previous_order_target = None; + let mut ambiguities = >::default(); + let mut target_counts = >::default(); + for sorted_camera in &mut sorted_cameras.0 { + let new_order_target = (sorted_camera.order, sorted_camera.target.clone()); + if let Some(previous_order_target) = previous_order_target { + if previous_order_target == new_order_target { + ambiguities.insert(new_order_target.clone()); + } + } + if let Some(target) = &sorted_camera.target { + let count = target_counts + .entry((target.clone(), sorted_camera.hdr)) + .or_insert(0usize); + let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap(); + camera.sorted_camera_index_for_target = *count; + *count += 1; + } + previous_order_target = Some(new_order_target); + } + + if !ambiguities.is_empty() { + warn!( + "Camera order ambiguities detected for active cameras with the following priorities: {:?}. \ + To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \ + Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \ + result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \ + ambiguities could result in unpredictable render results.", + ambiguities + ); + } +} + +/// A subpixel offset to jitter a perspective camera's frustum by. +/// +/// Useful for temporal rendering techniques. +/// +/// Do not use with [`OrthographicProjection`]. +/// +/// [`OrthographicProjection`]: crate::camera::OrthographicProjection +#[derive(Component, Clone, Default, Reflect)] +#[reflect(Default, Component, Clone)] +pub struct TemporalJitter { + /// Offset is in range [-0.5, 0.5]. + pub offset: Vec2, +} + +impl TemporalJitter { + pub fn jitter_projection(&self, clip_from_view: &mut Mat4, view_size: Vec2) { + if clip_from_view.w_axis.w == 1.0 { + warn!( + "TemporalJitter not supported with OrthographicProjection. Use PerspectiveProjection instead." + ); + return; + } + + // https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/docs/techniques/media/super-resolution-temporal/jitter-space.svg + let jitter = (self.offset * vec2(2.0, -2.0)) / view_size; + + clip_from_view.z_axis.x += jitter.x; + clip_from_view.z_axis.y += jitter.y; + } +} + +/// Camera component specifying a mip bias to apply when sampling from material textures. +/// +/// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness. +#[derive(Component, Reflect, Clone)] +#[reflect(Default, Component)] +pub struct MipBias(pub f32); + +impl Default for MipBias { + fn default() -> Self { + Self(-1.0) + } +} diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs deleted file mode 100644 index a2470a7660..0000000000 --- a/crates/bevy_render/src/camera/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -mod camera; -mod camera_driver_node; -mod clear_color; -mod manual_texture_view; -mod projection; - -pub use camera::*; -pub use camera_driver_node::*; -pub use clear_color::*; -pub use manual_texture_view::*; -pub use projection::*; - -use crate::{ - extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, - render_graph::RenderGraph, ExtractSchedule, Render, RenderApp, RenderSystems, -}; -use bevy_app::{App, Plugin}; -use bevy_ecs::schedule::IntoScheduleConfigs; - -#[derive(Default)] -pub struct CameraPlugin; - -impl Plugin for CameraPlugin { - fn build(&self, app: &mut App) { - app.register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .init_resource::() - .init_resource::() - .add_plugins(( - CameraProjectionPlugin, - ExtractResourcePlugin::::default(), - ExtractResourcePlugin::::default(), - ExtractComponentPlugin::::default(), - )); - - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::() - .add_systems(ExtractSchedule, extract_cameras) - .add_systems(Render, sort_cameras.in_set(RenderSystems::ManageViews)); - let camera_driver_node = CameraDriverNode::new(render_app.world_mut()); - let mut render_graph = render_app.world_mut().resource_mut::(); - render_graph.add_node(crate::graph::CameraDriverLabel, camera_driver_node); - } - } -} diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs index ec226c760b..e7005f70f3 100644 --- a/crates/bevy_render/src/diagnostic/internal.rs +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -15,7 +15,8 @@ use wgpu::{ PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, RenderPass, }; -use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper}; +use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue}; +use bevy_utils::WgpuWrapper; use super::RecordDiagnostics; 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/erased_render_asset.rs b/crates/bevy_render/src/erased_render_asset.rs new file mode 100644 index 0000000000..ac2423990b --- /dev/null +++ b/crates/bevy_render/src/erased_render_asset.rs @@ -0,0 +1,431 @@ +use crate::{ + render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, + RenderSystems, Res, +}; +use bevy_app::{App, Plugin, SubApp}; +pub use bevy_asset::RenderAssetUsages; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId}; +use bevy_ecs::{ + prelude::{Commands, EventReader, IntoScheduleConfigs, ResMut, Resource}, + schedule::{ScheduleConfigs, SystemSet}, + system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState}, + world::{FromWorld, Mut}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter; +use core::marker::PhantomData; +use thiserror::Error; +use tracing::{debug, error}; + +#[derive(Debug, Error)] +pub enum PrepareAssetError { + #[error("Failed to prepare asset")] + RetryNextUpdate(E), + #[error("Failed to build bind group: {0}")] + AsBindGroupError(AsBindGroupError), +} + +/// The system set during which we extract modified assets to the render world. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct AssetExtractionSystems; + +/// Deprecated alias for [`AssetExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")] +pub type ExtractAssetsSet = AssetExtractionSystems; + +/// Describes how an asset gets extracted and prepared for rendering. +/// +/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred +/// from the "main world" into the "render world". +/// +/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset +/// is transformed into its GPU-representation of type [`ErasedRenderAsset`]. +pub trait ErasedRenderAsset: Send + Sync + 'static { + /// The representation of the asset in the "main world". + type SourceAsset: Asset + Clone; + /// The target representation of the asset in the "render world". + type ErasedAsset: Send + Sync + 'static + Sized; + + /// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`]. + /// + /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`]. + type Param: SystemParam; + + /// Whether or not to unload the asset after extracting it to the render world. + #[inline] + fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages { + RenderAssetUsages::default() + } + + /// Size of the data the asset will upload to the gpu. Specifying a return value + /// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`]. + #[inline] + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn byte_len(erased_asset: &Self::SourceAsset) -> Option { + None + } + + /// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`]. + /// + /// ECS data may be accessed via `param`. + fn prepare_asset( + source_asset: Self::SourceAsset, + asset_id: AssetId, + param: &mut SystemParamItem, + ) -> Result>; + + /// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed. + /// + /// You can implement this method if you need to access ECS data (via + /// `_param`) in order to perform cleanup tasks when the asset is removed. + /// + /// The default implementation does nothing. + fn unload_asset( + _source_asset: AssetId, + _param: &mut SystemParamItem, + ) { + } +} + +/// This plugin extracts the changed assets from the "app world" into the "render world" +/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource. +/// +/// Therefore it sets up the [`ExtractSchedule`] and +/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`]. +/// +/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until +/// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another +/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::` for morph +/// targets, so the plugin is created as `ErasedRenderAssetPlugin::::default()`. +pub struct ErasedRenderAssetPlugin< + A: ErasedRenderAsset, + AFTER: ErasedRenderAssetDependency + 'static = (), +> { + phantom: PhantomData (A, AFTER)>, +} + +impl Default + for ErasedRenderAssetPlugin +{ + fn default() -> Self { + Self { + phantom: Default::default(), + } + } +} + +impl Plugin + for ErasedRenderAssetPlugin +{ + fn build(&self, app: &mut App) { + app.init_resource::>(); + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems( + ExtractSchedule, + extract_erased_render_asset::.in_set(AssetExtractionSystems), + ); + AFTER::register_system( + render_app, + prepare_erased_assets::.in_set(RenderSystems::PrepareAssets), + ); + } + } +} + +// helper to allow specifying dependencies between render assets +pub trait ErasedRenderAssetDependency { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs); +} + +impl ErasedRenderAssetDependency for () { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system); + } +} + +impl ErasedRenderAssetDependency for A { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system.after(prepare_erased_assets::)); + } +} + +/// Temporarily stores the extracted and removed assets of the current frame. +#[derive(Resource)] +pub struct ExtractedAssets { + /// The assets extracted this frame. + /// + /// These are assets that were either added or modified this frame. + pub extracted: Vec<(AssetId, A::SourceAsset)>, + + /// IDs of the assets that were removed this frame. + /// + /// These assets will not be present in [`ExtractedAssets::extracted`]. + pub removed: HashSet>, + + /// IDs of the assets that were modified this frame. + pub modified: HashSet>, + + /// IDs of the assets that were added this frame. + pub added: HashSet>, +} + +impl Default for ExtractedAssets { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + modified: Default::default(), + added: Default::default(), + } + } +} + +/// Stores all GPU representations ([`ErasedRenderAsset`]) +/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist. +#[derive(Resource)] +pub struct ErasedRenderAssets(HashMap); + +impl Default for ErasedRenderAssets { + fn default() -> Self { + Self(Default::default()) + } +} + +impl ErasedRenderAssets { + pub fn get(&self, id: impl Into) -> Option<&ERA> { + self.0.get(&id.into()) + } + + pub fn get_mut(&mut self, id: impl Into) -> Option<&mut ERA> { + self.0.get_mut(&id.into()) + } + + pub fn insert(&mut self, id: impl Into, value: ERA) -> Option { + self.0.insert(id.into(), value) + } + + pub fn remove(&mut self, id: impl Into) -> Option { + self.0.remove(&id.into()) + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(k, v)| (*k, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut().map(|(k, v)| (*k, v)) + } +} + +#[derive(Resource)] +struct CachedExtractErasedRenderAssetSystemState { + state: SystemState<( + EventReader<'static, 'static, AssetEvent>, + ResMut<'static, Assets>, + )>, +} + +impl FromWorld for CachedExtractErasedRenderAssetSystemState { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self { + state: SystemState::new(world), + } + } +} + +/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// into the "render world". +pub(crate) fn extract_erased_render_asset( + mut commands: Commands, + mut main_world: ResMut, +) { + main_world.resource_scope( + |world, mut cached_state: Mut>| { + let (mut events, mut assets) = cached_state.state.get_mut(world); + + let mut needs_extracting = >::default(); + let mut removed = >::default(); + let mut modified = >::default(); + + for event in events.read() { + #[expect( + clippy::match_same_arms, + reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." + )] + match event { + AssetEvent::Added { id } => { + needs_extracting.insert(*id); + } + AssetEvent::Modified { id } => { + needs_extracting.insert(*id); + modified.insert(*id); + } + AssetEvent::Removed { .. } => { + // We don't care that the asset was removed from Assets in the main world. + // An asset is only removed from ErasedRenderAssets when its last handle is dropped (AssetEvent::Unused). + } + AssetEvent::Unused { id } => { + needs_extracting.remove(id); + modified.remove(id); + removed.insert(*id); + } + AssetEvent::LoadedWithDependencies { .. } => { + // TODO: handle this + } + } + } + + let mut extracted_assets = Vec::new(); + let mut added = >::default(); + for id in needs_extracting.drain() { + if let Some(asset) = assets.get(id) { + let asset_usage = A::asset_usage(asset); + if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { + if asset_usage == RenderAssetUsages::RENDER_WORLD { + if let Some(asset) = assets.remove(id) { + extracted_assets.push((id, asset)); + added.insert(id); + } + } else { + extracted_assets.push((id, asset.clone())); + added.insert(id); + } + } + } + } + + commands.insert_resource(ExtractedAssets:: { + extracted: extracted_assets, + removed, + modified, + added, + }); + cached_state.state.apply(world); + }, + ); +} + +// TODO: consider storing inside system? +/// All assets that should be prepared next frame. +#[derive(Resource)] +pub struct PrepareNextFrameAssets { + assets: Vec<(AssetId, A::SourceAsset)>, +} + +impl Default for PrepareNextFrameAssets { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// which where extracted this frame for the GPU. +pub fn prepare_erased_assets( + mut extracted_assets: ResMut>, + mut render_assets: ResMut>, + mut prepare_next_frame: ResMut>, + param: StaticSystemParam<::Param>, + bpf: Res, +) { + let mut wrote_asset_count = 0; + + let mut param = param.into_inner(); + let queued_assets = core::mem::take(&mut prepare_next_frame.assets); + for (id, extracted_asset) in queued_assets { + if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) { + // skip previous frame's assets that have been removed or updated + continue; + } + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + // we could check if available bytes > byte_len here, but we want to make some + // forward progress even if the asset is larger than the max bytes per frame. + // this way we always write at least one (sized) asset per frame. + // in future we could also consider partial asset uploads. + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + for removed in extracted_assets.removed.drain() { + render_assets.remove(removed); + A::unload_asset(removed, &mut param); + } + + for (id, extracted_asset) in extracted_assets.extracted.drain(..) { + // 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 write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + if bpf.exhausted() && !prepare_next_frame.assets.is_empty() { + debug!( + "{} write budget exhausted with {} assets remaining (wrote {})", + core::any::type_name::(), + prepare_next_frame.assets.len(), + wrote_asset_count + ); + } +} diff --git a/crates/bevy_render/src/experimental/occlusion_culling/mod.rs b/crates/bevy_render/src/experimental/occlusion_culling/mod.rs index a3b067e19f..77fcb4b5b2 100644 --- a/crates/bevy_render/src/experimental/occlusion_culling/mod.rs +++ b/crates/bevy_render/src/experimental/occlusion_culling/mod.rs @@ -4,19 +4,13 @@ //! Bevy. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use crate::{ - extract_component::ExtractComponent, - render_resource::{Shader, TextureView}, + extract_component::ExtractComponent, load_shader_library, render_resource::TextureView, }; -/// The handle to the `mesh_preprocess_types.wgsl` compute shader. -pub const MESH_PREPROCESS_TYPES_SHADER_HANDLE: Handle = - weak_handle!("7bf7bdb1-ec53-4417-987f-9ec36533287c"); - /// Enables GPU occlusion culling. /// /// See [`OcclusionCulling`] for a detailed description of occlusion culling in @@ -25,12 +19,7 @@ pub struct OcclusionCullingPlugin; impl Plugin for OcclusionCullingPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - MESH_PREPROCESS_TYPES_SHADER_HANDLE, - "mesh_preprocess_types.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "mesh_preprocess_types.wgsl"); } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index e1f528d6ab..ce656c3579 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -3,10 +3,10 @@ use crate::{ renderer::{RenderDevice, RenderQueue}, sync_component::SyncComponentPlugin, sync_world::RenderEntity, - view::ViewVisibility, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; +use bevy_camera::visibility::ViewVisibility; use bevy_ecs::{ bundle::NoBundleEffect, component::Component, @@ -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_impls.rs b/crates/bevy_render/src/extract_impls.rs new file mode 100644 index 0000000000..87b854363a --- /dev/null +++ b/crates/bevy_render/src/extract_impls.rs @@ -0,0 +1,41 @@ +//! This module exists because of the orphan rule + +use bevy_ecs::query::QueryItem; +use bevy_light::{cluster::ClusteredDecal, AmbientLight, ShadowFilteringMethod}; + +use crate::{extract_component::ExtractComponent, extract_resource::ExtractResource}; + +impl ExtractComponent for ClusteredDecal { + type QueryData = &'static Self; + type QueryFilter = (); + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} +impl ExtractResource for AmbientLight { + type Source = Self; + + fn extract_resource(source: &Self::Source) -> Self { + source.clone() + } +} +impl ExtractComponent for AmbientLight { + type QueryData = &'static Self; + type QueryFilter = (); + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} +impl ExtractComponent for ShadowFilteringMethod { + type QueryData = &'static Self; + type QueryFilter = (); + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(*item) + } +} diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index a8e5a9ecbd..cf0d0c0cce 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -7,6 +7,7 @@ use core::marker::PhantomData; use bevy_app::{App, Plugin}; +use bevy_camera::visibility::ViewVisibility; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::Entity, @@ -16,7 +17,7 @@ use bevy_ecs::{ }; use crate::sync_world::MainEntityHashMap; -use crate::{prelude::ViewVisibility, Extract, ExtractSchedule, RenderApp}; +use crate::{Extract, ExtractSchedule, RenderApp}; /// Describes how to extract data needed for rendering from a component or /// components. @@ -34,7 +35,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 f543098474..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,17 +72,31 @@ where type State = ExtractState